@jagilber-org/index-server 1.22.1 → 1.26.4

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 (190) hide show
  1. package/CHANGELOG.md +91 -2
  2. package/CODE_OF_CONDUCT.md +2 -0
  3. package/CONTRIBUTING.md +32 -2
  4. package/README.md +82 -19
  5. package/SECURITY.md +17 -5
  6. package/dist/config/dashboardConfig.d.ts +3 -0
  7. package/dist/config/dashboardConfig.js +3 -0
  8. package/dist/config/defaultValues.d.ts +1 -1
  9. package/dist/config/defaultValues.js +1 -1
  10. package/dist/config/featureConfig.d.ts +2 -0
  11. package/dist/config/featureConfig.js +6 -1
  12. package/dist/config/runtimeConfig.d.ts +1 -1
  13. package/dist/config/runtimeConfig.js +8 -9
  14. package/dist/dashboard/client/admin.html +170 -53
  15. package/dist/dashboard/client/css/admin.css +132 -0
  16. package/dist/dashboard/client/js/admin.auth.js +25 -11
  17. package/dist/dashboard/client/js/admin.config.js +1 -1
  18. package/dist/dashboard/client/js/admin.feedback.js +328 -0
  19. package/dist/dashboard/client/js/admin.graph.js +120 -18
  20. package/dist/dashboard/client/js/admin.instructions.js +27 -13
  21. package/dist/dashboard/client/js/admin.logs.js +1 -5
  22. package/dist/dashboard/client/js/admin.maintenance.js +53 -8
  23. package/dist/dashboard/client/js/admin.messaging.js +1 -4
  24. package/dist/dashboard/client/js/admin.overview.js +5 -1
  25. package/dist/dashboard/client/js/admin.sessions.js +1 -1
  26. package/dist/dashboard/client/js/admin.utils.js +43 -1
  27. package/dist/dashboard/client/js/mermaid.min.js +813 -537
  28. package/dist/dashboard/export/DataExporter.js +2 -1
  29. package/dist/dashboard/server/AdminPanel.d.ts +3 -0
  30. package/dist/dashboard/server/AdminPanel.js +132 -35
  31. package/dist/dashboard/server/ApiRoutes.js +40 -9
  32. package/dist/dashboard/server/DashboardServer.js +1 -1
  33. package/dist/dashboard/server/FileMetricsStorage.d.ts +19 -0
  34. package/dist/dashboard/server/FileMetricsStorage.js +52 -5
  35. package/dist/dashboard/server/HttpTransport.js +6 -0
  36. package/dist/dashboard/server/InstanceManager.js +7 -2
  37. package/dist/dashboard/server/KnowledgeStore.js +7 -2
  38. package/dist/dashboard/server/MetricsCollector.d.ts +16 -0
  39. package/dist/dashboard/server/MetricsCollector.js +113 -17
  40. package/dist/dashboard/server/legacyDashboardHtml.js +7 -2
  41. package/dist/dashboard/server/middleware/ensureLoadedMiddleware.d.ts +1 -1
  42. package/dist/dashboard/server/middleware/ensureLoadedMiddleware.js +8 -3
  43. package/dist/dashboard/server/routes/admin.feedback.routes.d.ts +15 -0
  44. package/dist/dashboard/server/routes/admin.feedback.routes.js +188 -0
  45. package/dist/dashboard/server/routes/admin.routes.js +35 -27
  46. package/dist/dashboard/server/routes/alerts.routes.js +4 -3
  47. package/dist/dashboard/server/routes/api.feedback.routes.js +2 -1
  48. package/dist/dashboard/server/routes/api.usage.routes.js +8 -7
  49. package/dist/dashboard/server/routes/embeddings.routes.d.ts +2 -1
  50. package/dist/dashboard/server/routes/embeddings.routes.js +18 -9
  51. package/dist/dashboard/server/routes/graph.routes.js +10 -13
  52. package/dist/dashboard/server/routes/index.d.ts +1 -0
  53. package/dist/dashboard/server/routes/index.js +74 -39
  54. package/dist/dashboard/server/routes/instances.routes.js +2 -1
  55. package/dist/dashboard/server/routes/instructions.routes.js +46 -27
  56. package/dist/dashboard/server/routes/knowledge.routes.js +4 -3
  57. package/dist/dashboard/server/routes/logs.routes.js +5 -4
  58. package/dist/dashboard/server/routes/messaging.routes.js +15 -14
  59. package/dist/dashboard/server/routes/metrics.routes.js +14 -13
  60. package/dist/dashboard/server/routes/scripts.routes.js +6 -3
  61. package/dist/dashboard/server/routes/status.routes.js +5 -4
  62. package/dist/dashboard/server/routes/synthetic.routes.js +3 -2
  63. package/dist/dashboard/server/routes/usage.routes.js +2 -1
  64. package/dist/dashboard/server/utils/escapeHtml.d.ts +1 -0
  65. package/dist/dashboard/server/utils/escapeHtml.js +11 -0
  66. package/dist/dashboard/server/utils/pathContainment.d.ts +1 -0
  67. package/dist/dashboard/server/utils/pathContainment.js +15 -0
  68. package/dist/dashboard/server/wsInit.js +2 -2
  69. package/dist/lib/mcpStdioLogging.d.ts +165 -0
  70. package/dist/lib/mcpStdioLogging.js +287 -0
  71. package/dist/schemas/index.d.ts +37 -2
  72. package/dist/schemas/index.js +27 -3
  73. package/dist/server/backgroundServicesStartup.d.ts +7 -1
  74. package/dist/server/backgroundServicesStartup.js +25 -8
  75. package/dist/server/certInit.d.ts +97 -0
  76. package/dist/server/certInit.js +359 -0
  77. package/dist/server/certInit.types.d.ts +92 -0
  78. package/dist/server/certInit.types.js +34 -0
  79. package/dist/server/handshake/fallbackFrames.d.ts +31 -0
  80. package/dist/server/handshake/fallbackFrames.js +38 -0
  81. package/dist/server/handshake/initializeDetector.d.ts +31 -0
  82. package/dist/server/handshake/initializeDetector.js +88 -0
  83. package/dist/server/handshake/protocol.d.ts +15 -0
  84. package/dist/server/handshake/protocol.js +37 -0
  85. package/dist/server/handshake/readyEmitter.d.ts +6 -0
  86. package/dist/server/handshake/readyEmitter.js +88 -0
  87. package/dist/server/handshake/safetyFallbacks.d.ts +1 -0
  88. package/dist/server/handshake/safetyFallbacks.js +134 -0
  89. package/dist/server/handshake/stdinSniffer.d.ts +1 -0
  90. package/dist/server/handshake/stdinSniffer.js +260 -0
  91. package/dist/server/handshake/tracing.d.ts +16 -0
  92. package/dist/server/handshake/tracing.js +95 -0
  93. package/dist/server/handshakeManager.d.ts +23 -23
  94. package/dist/server/handshakeManager.js +36 -466
  95. package/dist/server/index-server.d.ts +23 -0
  96. package/dist/server/index-server.js +194 -9
  97. package/dist/server/mcpReadOnlySurfaces.d.ts +44 -0
  98. package/dist/server/mcpReadOnlySurfaces.js +297 -0
  99. package/dist/server/sdkServer.js +69 -7
  100. package/dist/server/transport.d.ts +5 -6
  101. package/dist/server/transport.js +46 -64
  102. package/dist/server/transportFactory.d.ts +3 -9
  103. package/dist/server/transportFactory.js +18 -380
  104. package/dist/services/atomicFs.d.ts +3 -0
  105. package/dist/services/atomicFs.js +171 -13
  106. package/dist/services/auditLog.d.ts +17 -2
  107. package/dist/services/auditLog.js +75 -14
  108. package/dist/services/bootstrapGating.js +1 -1
  109. package/dist/services/categoryRules.d.ts +10 -0
  110. package/dist/services/categoryRules.js +17 -0
  111. package/dist/services/classificationService.js +7 -5
  112. package/dist/services/embeddingService.d.ts +27 -11
  113. package/dist/services/embeddingService.js +51 -14
  114. package/dist/services/feedbackStorage.d.ts +39 -0
  115. package/dist/services/feedbackStorage.js +88 -0
  116. package/dist/services/handlers/instructions.add.js +429 -317
  117. package/dist/services/handlers/instructions.groom.js +128 -31
  118. package/dist/services/handlers/instructions.import.js +56 -23
  119. package/dist/services/handlers/instructions.patch.js +43 -32
  120. package/dist/services/handlers/instructions.query.js +20 -29
  121. package/dist/services/handlers/instructions.shared.d.ts +54 -0
  122. package/dist/services/handlers/instructions.shared.js +126 -1
  123. package/dist/services/handlers.activation.js +83 -81
  124. package/dist/services/handlers.dashboardConfig.d.ts +2 -2
  125. package/dist/services/handlers.dashboardConfig.js +1 -2
  126. package/dist/services/handlers.diagnostics.js +75 -54
  127. package/dist/services/handlers.feedback.d.ts +4 -11
  128. package/dist/services/handlers.feedback.js +11 -333
  129. package/dist/services/handlers.gates.js +69 -37
  130. package/dist/services/handlers.graph.js +2 -2
  131. package/dist/services/handlers.help.js +2 -2
  132. package/dist/services/handlers.instructionSchema.js +4 -2
  133. package/dist/services/handlers.integrity.js +42 -22
  134. package/dist/services/handlers.messaging.js +1 -1
  135. package/dist/services/handlers.metrics.js +51 -6
  136. package/dist/services/handlers.prompt.js +10 -2
  137. package/dist/services/handlers.search.js +94 -44
  138. package/dist/services/handlers.trace.js +1 -1
  139. package/dist/services/handlers.usage.js +38 -7
  140. package/dist/services/indexContext.d.ts +21 -1
  141. package/dist/services/indexContext.js +263 -78
  142. package/dist/services/indexLoader.d.ts +1 -0
  143. package/dist/services/indexLoader.js +28 -8
  144. package/dist/services/instructionRecordValidation.d.ts +39 -0
  145. package/dist/services/instructionRecordValidation.js +388 -0
  146. package/dist/services/instructions.dispatcher.js +4 -4
  147. package/dist/services/loaderSchemaValidator.d.ts +15 -0
  148. package/dist/services/loaderSchemaValidator.js +69 -0
  149. package/dist/services/logger.js +11 -2
  150. package/dist/services/mcpLogBridge.d.ts +49 -0
  151. package/dist/services/mcpLogBridge.js +83 -0
  152. package/dist/services/ownershipService.js +18 -8
  153. package/dist/services/performanceBaseline.js +23 -22
  154. package/dist/services/promptReviewService.d.ts +3 -1
  155. package/dist/services/promptReviewService.js +41 -13
  156. package/dist/services/regexSafety.d.ts +6 -0
  157. package/dist/services/regexSafety.js +46 -0
  158. package/dist/services/seedBootstrap.js +1 -1
  159. package/dist/services/storage/factory.d.ts +14 -1
  160. package/dist/services/storage/factory.js +61 -1
  161. package/dist/services/storage/jsonEmbeddingStore.d.ts +15 -0
  162. package/dist/services/storage/jsonEmbeddingStore.js +83 -0
  163. package/dist/services/storage/jsonFileStore.d.ts +3 -1
  164. package/dist/services/storage/jsonFileStore.js +8 -6
  165. package/dist/services/storage/migrationEngine.d.ts +13 -0
  166. package/dist/services/storage/migrationEngine.js +31 -0
  167. package/dist/services/storage/sqliteEmbeddingStore.d.ts +30 -0
  168. package/dist/services/storage/sqliteEmbeddingStore.js +222 -0
  169. package/dist/services/storage/sqliteStore.d.ts +3 -1
  170. package/dist/services/storage/sqliteStore.js +2 -2
  171. package/dist/services/storage/types.d.ts +48 -1
  172. package/dist/services/toolRegistry.js +77 -67
  173. package/dist/services/toolRegistry.zod.js +89 -86
  174. package/dist/services/tracing.js +5 -4
  175. package/dist/utils/envUtils.d.ts +4 -0
  176. package/dist/utils/envUtils.js +7 -0
  177. package/dist/utils/memoryMonitor.js +11 -10
  178. package/package.json +12 -4
  179. package/schemas/instruction.schema.json +38 -1
  180. package/scripts/copy-dashboard-assets.mjs +1 -1
  181. package/scripts/dist/README.md +1 -1
  182. package/scripts/generate-certs.mjs +201 -0
  183. package/scripts/setup-wizard.mjs +781 -0
  184. package/server.json +20 -0
  185. package/dist/externalClientLib.d.ts +0 -1
  186. package/dist/externalClientLib.js +0 -2
  187. package/dist/portableClientWrapper.d.ts +0 -1
  188. package/dist/portableClientWrapper.js +0 -2
  189. package/dist/services/indexingService.d.ts +0 -1
  190. package/dist/services/indexingService.js +0 -2
@@ -12,6 +12,21 @@ export interface AuditEntry {
12
12
  ids?: string[];
13
13
  meta?: Record<string, unknown>;
14
14
  }
15
+ export interface AuditReadResult {
16
+ entries: AuditEntry[];
17
+ parseErrors: number;
18
+ }
19
+ export interface AuditLogHealth {
20
+ enabled: boolean;
21
+ file: string | null;
22
+ writeFailures: number;
23
+ readFailures: number;
24
+ parseErrors: number;
25
+ degraded: boolean;
26
+ lastWriteError?: string;
27
+ lastReadError?: string;
28
+ }
29
+ export declare function getAuditLogHealth(): AuditLogHealth;
15
30
  /**
16
31
  * Append an entry to the audit log file. Silent no-op when logging is disabled.
17
32
  * @param action - Tool or operation name being recorded
@@ -33,6 +48,6 @@ export declare function logHttpAudit(method: string, route: string, statusCode:
33
48
  /**
34
49
  * Read the most recent audit log entries from disk.
35
50
  * @param limit - Maximum number of lines to return from the tail of the file (default 1000)
36
- * @returns Parsed audit entries, or an empty array if logging is disabled or the file is missing
51
+ * @returns Parsed audit entries plus parse-error count, or an empty result if logging is disabled or the file is missing
37
52
  */
38
- export declare function readAuditEntries(limit?: number): AuditEntry[];
53
+ export declare function readAuditEntries(limit?: number): AuditReadResult;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runWithCorrelation = runWithCorrelation;
7
7
  exports.getCurrentCorrelationId = getCurrentCorrelationId;
8
8
  exports.resetAuditLogCache = resetAuditLogCache;
9
+ exports.getAuditLogHealth = getAuditLogHealth;
9
10
  exports.logAudit = logAudit;
10
11
  exports.logToolAudit = logToolAudit;
11
12
  exports.logHttpAudit = logHttpAudit;
@@ -14,6 +15,7 @@ const fs_1 = __importDefault(require("fs"));
14
15
  const path_1 = __importDefault(require("path"));
15
16
  const async_hooks_1 = require("async_hooks");
16
17
  const runtimeConfig_1 = require("../config/runtimeConfig");
18
+ const logger_1 = require("./logger");
17
19
  const toolRegistry_1 = require("./toolRegistry");
18
20
  // Append-only JSONL audit log for all server operations.
19
21
  // Each line: { ts, kind, action, ids?, meta? }
@@ -33,6 +35,35 @@ function getCurrentCorrelationId() {
33
35
  }
34
36
  let cachedKey;
35
37
  let cachedPath;
38
+ const auditLogState = {
39
+ writeFailures: 0,
40
+ readFailures: 0,
41
+ parseErrors: 0,
42
+ writeWarningActive: false,
43
+ };
44
+ function formatAuditError(error) {
45
+ return error instanceof Error ? error.message : String(error);
46
+ }
47
+ function markAuditWriteFailure(file, error) {
48
+ auditLogState.writeFailures += 1;
49
+ auditLogState.lastWriteError = formatAuditError(error);
50
+ if (!auditLogState.writeWarningActive) {
51
+ auditLogState.writeWarningActive = true;
52
+ (0, logger_1.logWarn)('[audit] Failed to persist audit log; entries may be dropped until persistence recovers', {
53
+ file,
54
+ error: auditLogState.lastWriteError,
55
+ writeFailures: auditLogState.writeFailures,
56
+ });
57
+ }
58
+ }
59
+ function markAuditWriteSuccess() {
60
+ auditLogState.lastWriteError = undefined;
61
+ auditLogState.writeWarningActive = false;
62
+ }
63
+ function markAuditReadFailure(error) {
64
+ auditLogState.readFailures += 1;
65
+ auditLogState.lastReadError = formatAuditError(error);
66
+ }
36
67
  function resolveLogPath() {
37
68
  const { auditLog } = (0, runtimeConfig_1.getRuntimeConfig)().instructions;
38
69
  const key = auditLog.enabled && auditLog.file ? `on:${auditLog.file}` : 'off';
@@ -52,8 +83,10 @@ function resolveLogPath() {
52
83
  fs_1.default.appendFileSync(file, '');
53
84
  cachedPath = file;
54
85
  }
55
- catch {
56
- cachedPath = null;
86
+ catch (error) {
87
+ cachedPath = undefined;
88
+ markAuditWriteFailure(file, error);
89
+ return null;
57
90
  }
58
91
  return cachedPath;
59
92
  }
@@ -61,6 +94,25 @@ function resolveLogPath() {
61
94
  function resetAuditLogCache() {
62
95
  cachedKey = undefined;
63
96
  cachedPath = undefined;
97
+ auditLogState.writeFailures = 0;
98
+ auditLogState.readFailures = 0;
99
+ auditLogState.parseErrors = 0;
100
+ auditLogState.lastWriteError = undefined;
101
+ auditLogState.lastReadError = undefined;
102
+ auditLogState.writeWarningActive = false;
103
+ }
104
+ function getAuditLogHealth() {
105
+ const { auditLog } = (0, runtimeConfig_1.getRuntimeConfig)().instructions;
106
+ return {
107
+ enabled: Boolean(auditLog.enabled && auditLog.file),
108
+ file: auditLog.file ?? null,
109
+ writeFailures: auditLogState.writeFailures,
110
+ readFailures: auditLogState.readFailures,
111
+ parseErrors: auditLogState.parseErrors,
112
+ degraded: auditLogState.writeFailures > 0 || auditLogState.readFailures > 0 || auditLogState.parseErrors > 0,
113
+ lastWriteError: auditLogState.lastWriteError,
114
+ lastReadError: auditLogState.lastReadError,
115
+ };
64
116
  }
65
117
  /**
66
118
  * Append an entry to the audit log file. Silent no-op when logging is disabled.
@@ -72,11 +124,10 @@ function resetAuditLogCache() {
72
124
  function logAudit(action, ids, meta, kind) {
73
125
  const file = resolveLogPath();
74
126
  if (!file)
75
- return; // silent no-op when logging disabled
127
+ return; // silent no-op when logging is disabled
76
128
  const entry = { ts: new Date().toISOString(), kind: kind ?? 'mutation', action };
77
- if (ids) {
129
+ if (ids)
78
130
  entry.ids = Array.isArray(ids) ? ids : [ids];
79
- }
80
131
  // Auto-inject correlationId from async context if not already present in meta
81
132
  const ctxCorr = getCurrentCorrelationId();
82
133
  if (ctxCorr || meta) {
@@ -87,8 +138,12 @@ function logAudit(action, ids, meta, kind) {
87
138
  }
88
139
  try {
89
140
  fs_1.default.appendFileSync(file, JSON.stringify(entry) + '\n', 'utf8'); // lgtm[js/http-to-file-access] — audit log path from config
141
+ markAuditWriteSuccess();
142
+ }
143
+ catch (error) {
144
+ cachedPath = undefined;
145
+ markAuditWriteFailure(file, error);
90
146
  }
91
- catch { /* swallow logging errors to avoid impacting primary operation path */ }
92
147
  }
93
148
  /**
94
149
  * Log a tool invocation to the audit trail. Called from the registry wrapper
@@ -118,25 +173,31 @@ function logHttpAudit(method, route, statusCode, durationMs, clientIp, userAgent
118
173
  /**
119
174
  * Read the most recent audit log entries from disk.
120
175
  * @param limit - Maximum number of lines to return from the tail of the file (default 1000)
121
- * @returns Parsed audit entries, or an empty array if logging is disabled or the file is missing
176
+ * @returns Parsed audit entries plus parse-error count, or an empty result if logging is disabled or the file is missing
122
177
  */
123
178
  function readAuditEntries(limit = 1000) {
124
179
  const file = resolveLogPath();
125
180
  if (!file || !fs_1.default.existsSync(file))
126
- return [];
181
+ return { entries: [], parseErrors: 0 };
127
182
  try {
128
183
  const lines = fs_1.default.readFileSync(file, 'utf8').split(/\r?\n/).filter(l => l.trim());
129
184
  const recent = lines.slice(-limit);
130
185
  const parsed = [];
131
- for (const l of recent) {
186
+ let parseErrors = 0;
187
+ for (const line of recent) {
132
188
  try {
133
- parsed.push(JSON.parse(l));
189
+ parsed.push(JSON.parse(line));
190
+ }
191
+ catch {
192
+ parseErrors += 1;
134
193
  }
135
- catch { /* ignore */ }
136
194
  }
137
- return parsed;
195
+ auditLogState.parseErrors += parseErrors;
196
+ auditLogState.lastReadError = undefined;
197
+ return { entries: parsed, parseErrors };
138
198
  }
139
- catch {
140
- return [];
199
+ catch (error) {
200
+ markAuditReadFailure(error);
201
+ return { entries: [], parseErrors: 0 };
141
202
  }
142
203
  }
@@ -23,7 +23,7 @@ const runtimeConfig_1 = require("../config/runtimeConfig");
23
23
  * States:
24
24
  * - reference mode (INDEX_SERVER_REFERENCE_MODE=1): index is read-only, all mutation blocked permanently
25
25
  * - new workspace: only bootstrap seed instructions present (000-bootstrapper / 001-lifecycle-bootstrap) → require confirmation
26
- * - existing workspace: any non-bootstrap instruction present OR confirmation file exists -> mutation allowed (subject to INDEX_SERVER_MUTATION rules)
26
+ * - existing workspace: any non-bootstrap instruction present OR confirmation file exists -> mutation allowed (unless explicitly forced read-only)
27
27
  * - confirmed: confirmation file created after successful finalize → persists across restarts
28
28
  */
29
29
  const BOOTSTRAP_IDS = new Set(['000-bootstrapper', '001-lifecycle-bootstrap']);
@@ -5,3 +5,13 @@
5
5
  */
6
6
  export declare const CATEGORY_RULES: [RegExp, string][];
7
7
  export declare function deriveCategory(id: string): string;
8
+ /**
9
+ * Convert a CATEGORY_RULES display label (e.g. "Service Fabric", "VS Code",
10
+ * "AI/ML", ".NET", "Git/Repo") into a token that satisfies the disk schema's
11
+ * category pattern (^[a-z0-9][a-z0-9-_]{0,48}$ — lowercase, hyphen/underscore
12
+ * only, must start with [a-z0-9]).
13
+ *
14
+ * Used by groom remapCategories before writing primaryCategory to disk so the
15
+ * loader-symmetric validator does not silently reject the rewrite.
16
+ */
17
+ export declare function slugifyCategory(label: string): string;
@@ -7,6 +7,7 @@
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.CATEGORY_RULES = void 0;
9
9
  exports.deriveCategory = deriveCategory;
10
+ exports.slugifyCategory = slugifyCategory;
10
11
  exports.CATEGORY_RULES = [
11
12
  [/azure|arm-template|apim|batch|vmss/, 'Azure'],
12
13
  [/\bsf[-_]|service-fabric|servicefabric|collectsf|sfrp/, 'Service Fabric'],
@@ -35,3 +36,19 @@ function deriveCategory(id) {
35
36
  }
36
37
  return 'Other';
37
38
  }
39
+ /**
40
+ * Convert a CATEGORY_RULES display label (e.g. "Service Fabric", "VS Code",
41
+ * "AI/ML", ".NET", "Git/Repo") into a token that satisfies the disk schema's
42
+ * category pattern (^[a-z0-9][a-z0-9-_]{0,48}$ — lowercase, hyphen/underscore
43
+ * only, must start with [a-z0-9]).
44
+ *
45
+ * Used by groom remapCategories before writing primaryCategory to disk so the
46
+ * loader-symmetric validator does not silently reject the rewrite.
47
+ */
48
+ function slugifyCategory(label) {
49
+ return label
50
+ .toLowerCase()
51
+ .replace(/[^a-z0-9]+/g, '-') // collapse runs of non-alphanumerics
52
+ .replace(/^-+|-+$/g, '') // strip leading/trailing hyphens
53
+ .slice(0, 49); // schema allows up to 49 chars
54
+ }
@@ -22,7 +22,7 @@ class ClassificationService {
22
22
  let userId = entry.userId;
23
23
  const teamIds = entry.teamIds ? [...entry.teamIds] : [];
24
24
  const otherCats = [];
25
- for (const cRaw of entry.categories) {
25
+ for (const cRaw of entry.categories ?? []) {
26
26
  const c = cRaw.toLowerCase();
27
27
  if (c.startsWith('scope:workspace:')) {
28
28
  if (!workspaceId)
@@ -61,8 +61,12 @@ class ClassificationService {
61
61
  categories: Array.from(new Set(otherCats.map(c => c.toLowerCase()))).sort(),
62
62
  updatedAt: entry.updatedAt || now,
63
63
  createdAt: entry.createdAt || now,
64
- // Guarantee schemaVersion presence (tests/assertions now rely on dispatcher list action output)
65
- schemaVersion: entry.schemaVersion || schemaVersion_1.SCHEMA_VERSION,
64
+ // Always coerce to current SCHEMA_VERSION on normalization. The write path
65
+ // (writeEntry / writeEntryAsync) validates against the loader JSON schema
66
+ // which only accepts the current version. Preserving a legacy version here
67
+ // would cause silent write rejection. Migration of legacy fields runs
68
+ // separately via migrateInstructionRecord on the write path.
69
+ schemaVersion: schemaVersion_1.SCHEMA_VERSION,
66
70
  // Compute hash from canonical (trimmed) body to ensure stability across innocuous whitespace differences
67
71
  sourceHash: entry.sourceHash && entry.sourceHash.length === 64 ? entry.sourceHash : this.computeHash(trimmedBody),
68
72
  riskScore: this.computeRisk(entry),
@@ -97,8 +101,6 @@ class ClassificationService {
97
101
  issues.push('missing title');
98
102
  if (!entry.body)
99
103
  issues.push('missing body');
100
- if (entry.requirement === 'deprecated' && !entry.deprecatedBy)
101
- issues.push('deprecated requires deprecatedBy');
102
104
  return issues;
103
105
  }
104
106
  /**
@@ -11,6 +11,8 @@
11
11
  * - Uses @huggingface/transformers (optional dep) via dynamic ESM import
12
12
  */
13
13
  import { InstructionEntry } from '../models/instruction';
14
+ import type { EmbeddingCacheData, IEmbeddingStore } from './storage/types';
15
+ export type { EmbeddingCacheData } from './storage/types';
14
16
  /**
15
17
  * Cosine similarity between two vectors.
16
18
  * Pure math — no dependencies, no model needed.
@@ -20,16 +22,6 @@ export declare function cosineSimilarity(a: Float32Array, b: Float32Array): numb
20
22
  * Check if cached embeddings are stale (index has changed since last embed).
21
23
  */
22
24
  export declare function isStale(indexHash: string, embeddingHash: string): boolean;
23
- /**
24
- * Persisted embedding cache format (backwards-compatible: entryHashes is optional).
25
- */
26
- export interface EmbeddingCacheData {
27
- indexHash: string;
28
- modelName?: string;
29
- /** Per-entry content hashes for incremental invalidation (required for v2+). */
30
- entryHashes?: Record<string, string>;
31
- embeddings: Record<string, number[]>;
32
- }
33
25
  /**
34
26
  * Load cached embeddings from disk.
35
27
  * Returns null if file doesn't exist or is invalid.
@@ -39,11 +31,35 @@ export declare function loadCachedEmbeddings(filePath: string): EmbeddingCacheDa
39
31
  * Save embeddings to disk for cross-instance sharing.
40
32
  */
41
33
  export declare function saveCachedEmbeddings(filePath: string, data: EmbeddingCacheData): void;
34
+ /**
35
+ * Check available ONNX Runtime execution providers and resolve the best device.
36
+ * Falls back to dml → cpu if the requested provider is not available.
37
+ *
38
+ * @returns The resolved device string ('cpu', 'cuda', or 'dml').
39
+ */
40
+ /** ORT module shape accepted by resolveDevice for testability. */
41
+ export interface OrtModule {
42
+ listSupportedBackends?: () => Array<{
43
+ name: string;
44
+ bundled: boolean;
45
+ }>;
46
+ }
47
+ export declare function resolveDevice(requested: string, ortModule?: OrtModule): Promise<string>;
42
48
  /**
43
49
  * Embed a single text string into a vector.
44
50
  * Triggers lazy model loading on first call.
45
51
  */
46
52
  export declare function embedText(text: string, modelName: string, cacheDir: string, device?: string, localOnly?: boolean): Promise<Float32Array>;
53
+ /**
54
+ * Check if the embedding model is ready for use.
55
+ * When localOnly is true, verifies model files exist in the cache directory.
56
+ *
57
+ * @returns Object with `ready` flag and optional remediation `message`.
58
+ */
59
+ export declare function checkModelReadiness(modelName: string, cacheDir: string, localOnly: boolean): {
60
+ ready: boolean;
61
+ message?: string;
62
+ };
47
63
  /** Signature for the embed function (injectable for testing). */
48
64
  export type EmbedFn = (text: string, modelName: string, cacheDir: string, device: string, localOnly: boolean) => Promise<Float32Array>;
49
65
  /**
@@ -59,4 +75,4 @@ export type EmbedFn = (text: string, modelName: string, cacheDir: string, device
59
75
  *
60
76
  * @param embedFn - Injectable embed function (defaults to module embedText; override in tests).
61
77
  */
62
- export declare function getInstructionEmbeddings(instructions: InstructionEntry[], indexHash: string, embeddingPath: string, modelName: string, cacheDir: string, device?: string, localOnly?: boolean, embedFn?: EmbedFn): Promise<Record<string, Float32Array>>;
78
+ export declare function getInstructionEmbeddings(instructions: InstructionEntry[], indexHash: string, embeddingPath: string, modelName: string, cacheDir: string, device?: string, localOnly?: boolean, embedFn?: EmbedFn, store?: IEmbeddingStore): Promise<Record<string, Float32Array>>;
@@ -19,7 +19,9 @@ exports.cosineSimilarity = cosineSimilarity;
19
19
  exports.isStale = isStale;
20
20
  exports.loadCachedEmbeddings = loadCachedEmbeddings;
21
21
  exports.saveCachedEmbeddings = saveCachedEmbeddings;
22
+ exports.resolveDevice = resolveDevice;
22
23
  exports.embedText = embedText;
24
+ exports.checkModelReadiness = checkModelReadiness;
23
25
  exports.getInstructionEmbeddings = getInstructionEmbeddings;
24
26
  const fs_1 = __importDefault(require("fs"));
25
27
  const path_1 = __importDefault(require("path"));
@@ -86,7 +88,7 @@ function saveCachedEmbeddings(filePath, data) {
86
88
  if (!fs_1.default.existsSync(dir)) {
87
89
  fs_1.default.mkdirSync(dir, { recursive: true });
88
90
  }
89
- fs_1.default.writeFileSync(filePath, JSON.stringify(data), 'utf-8');
91
+ fs_1.default.writeFileSync(filePath, JSON.stringify(data), 'utf-8'); // lgtm[js/http-to-file-access] — filePath is config-controlled embedding cache path
90
92
  }
91
93
  // Helper: dynamic ESM import (same pattern as sdkServer.ts)
92
94
  const dynamicImport = (specifier) => (Function('m', 'return import(m);'))(specifier);
@@ -136,15 +138,11 @@ async function ensureModel(modelName, cacheDir, device = 'cpu', localOnly = fals
136
138
  })();
137
139
  await modelLoading;
138
140
  }
139
- /**
140
- * Check available ONNX Runtime execution providers and resolve the best device.
141
- * Falls back to dml → cpu if the requested provider is not available.
142
- */
143
- async function resolveDevice(requested) {
141
+ async function resolveDevice(requested, ortModule) {
144
142
  if (requested === 'cpu')
145
143
  return 'cpu';
146
144
  try {
147
- const ort = await dynamicImport('onnxruntime-node');
145
+ const ort = ortModule ?? await dynamicImport('onnxruntime-node');
148
146
  if (typeof ort.listSupportedBackends === 'function') {
149
147
  const backends = ort.listSupportedBackends();
150
148
  const available = backends.map((b) => b.name);
@@ -160,11 +158,15 @@ async function resolveDevice(requested) {
160
158
  (0, logger_1.logWarn)(`[embeddingService] ${requested} provider not available. Available: [${available.join(', ')}]. Falling back to cpu.`);
161
159
  return 'cpu';
162
160
  }
161
+ else {
162
+ (0, logger_1.logWarn)(`[embeddingService] onnxruntime-node does not expose listSupportedBackends(). Falling back to cpu.`);
163
+ return 'cpu';
164
+ }
163
165
  }
164
166
  catch {
165
- (0, logger_1.logWarn)(`[embeddingService] Could not probe ONNX Runtime backends; using requested device '${requested}' as-is.`);
167
+ (0, logger_1.logWarn)(`[embeddingService] Could not probe ONNX Runtime backends (onnxruntime-node not installed or import failed). Falling back to cpu.`);
166
168
  }
167
- return requested;
169
+ return 'cpu';
168
170
  }
169
171
  /**
170
172
  * Embed a single text string into a vector.
@@ -178,6 +180,35 @@ async function embedText(text, modelName, cacheDir, device = 'cpu', localOnly =
178
180
  (0, logger_1.logInfo)(`[embeddingService] embedText completed in ${(performance.now() - start).toFixed(1)}ms (${text.substring(0, 60)}${text.length > 60 ? '...' : ''})`);
179
181
  return vec;
180
182
  }
183
+ /**
184
+ * Check if the embedding model is ready for use.
185
+ * When localOnly is true, verifies model files exist in the cache directory.
186
+ *
187
+ * @returns Object with `ready` flag and optional remediation `message`.
188
+ */
189
+ function checkModelReadiness(modelName, cacheDir, localOnly) {
190
+ if (!localOnly) {
191
+ return { ready: true }; // Model can be downloaded on demand
192
+ }
193
+ // HuggingFace transformers caches models as: models--<org>--<name>
194
+ const modelDirName = `models--${modelName.replace(/\//g, '--')}`;
195
+ const modelPath = path_1.default.join(cacheDir, modelDirName);
196
+ try {
197
+ if (fs_1.default.existsSync(modelPath) && fs_1.default.readdirSync(modelPath).length > 0) {
198
+ return { ready: true };
199
+ }
200
+ }
201
+ catch {
202
+ // Directory doesn't exist or can't be read
203
+ }
204
+ return {
205
+ ready: false,
206
+ message: `Embedding model '${modelName}' not found in cache (${cacheDir}). ` +
207
+ `LOCAL_ONLY is enabled, so the model cannot be downloaded automatically. ` +
208
+ `To fix: set INDEX_SERVER_SEMANTIC_LOCAL_ONLY=0 to allow download, ` +
209
+ `or manually place the model in the cache directory.`,
210
+ };
211
+ }
181
212
  /**
182
213
  * Get or compute embeddings for all index instructions.
183
214
  * Uses disk cache when available and fresh.
@@ -191,9 +222,9 @@ async function embedText(text, modelName, cacheDir, device = 'cpu', localOnly =
191
222
  *
192
223
  * @param embedFn - Injectable embed function (defaults to module embedText; override in tests).
193
224
  */
194
- async function getInstructionEmbeddings(instructions, indexHash, embeddingPath, modelName, cacheDir, device = 'cpu', localOnly = false, embedFn = embedText) {
225
+ async function getInstructionEmbeddings(instructions, indexHash, embeddingPath, modelName, cacheDir, device = 'cpu', localOnly = false, embedFn = embedText, store) {
195
226
  // Full cache hit: same index hash and model — return immediately without locking.
196
- const cached = loadCachedEmbeddings(embeddingPath);
227
+ const cached = store ? store.load() : loadCachedEmbeddings(embeddingPath);
197
228
  if (cached && !isStale(indexHash, cached.indexHash) && cached.modelName === modelName) {
198
229
  const entryCount = Object.keys(cached.embeddings).length;
199
230
  (0, logger_1.logInfo)(`[embeddingService] Embedding cache HIT: ${entryCount} entries from ${embeddingPath} (model=${modelName})`);
@@ -230,9 +261,9 @@ async function getInstructionEmbeddings(instructions, indexHash, embeddingPath,
230
261
  const entryHashes = { ...existingHashes };
231
262
  for (const inst of toCompute) {
232
263
  const text = `${inst.title} ${inst.semanticSummary || inst.body}`;
233
- embeddings[inst.id] = Array.from(await embedFn(text, modelName, cacheDir, device, localOnly));
264
+ embeddings[inst.id] = Array.from(await embedFn(text, modelName, cacheDir, device, localOnly)); // lgtm[js/remote-property-injection] — id is schema-validated before reaching index
234
265
  if (inst.sourceHash)
235
- entryHashes[inst.id] = inst.sourceHash;
266
+ entryHashes[inst.id] = inst.sourceHash; // lgtm[js/remote-property-injection] — id is schema-validated before reaching index
236
267
  }
237
268
  // Prune deleted instructions from cache.
238
269
  for (const id of Object.keys(embeddings)) {
@@ -243,7 +274,13 @@ async function getInstructionEmbeddings(instructions, indexHash, embeddingPath,
243
274
  }
244
275
  // Persist updated cache.
245
276
  try {
246
- saveCachedEmbeddings(embeddingPath, { indexHash, modelName, entryHashes, embeddings });
277
+ const cacheData = { indexHash, modelName, entryHashes, embeddings };
278
+ if (store) {
279
+ store.save(cacheData);
280
+ }
281
+ else {
282
+ saveCachedEmbeddings(embeddingPath, cacheData);
283
+ }
247
284
  (0, logger_1.logInfo)('[embeddingService] Embeddings cached to disk');
248
285
  }
249
286
  catch (err) {
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Shared feedback storage layer.
3
+ * Used by both the MCP feedback_submit handler and the future dashboard CRUD REST routes.
4
+ * All file I/O lives here — no duplication across consumers.
5
+ */
6
+ export interface FeedbackEntry {
7
+ id: string;
8
+ timestamp: string;
9
+ type: 'issue' | 'status' | 'security' | 'feature-request' | 'bug-report' | 'performance' | 'usability' | 'other';
10
+ severity: 'low' | 'medium' | 'high' | 'critical';
11
+ title: string;
12
+ description: string;
13
+ context?: {
14
+ clientInfo?: {
15
+ name: string;
16
+ version: string;
17
+ };
18
+ serverVersion?: string;
19
+ environment?: Record<string, string>;
20
+ sessionId?: string;
21
+ toolName?: string;
22
+ requestId?: string;
23
+ };
24
+ metadata?: Record<string, unknown>;
25
+ tags?: string[];
26
+ status: 'new' | 'acknowledged' | 'in-progress' | 'resolved' | 'closed';
27
+ }
28
+ export interface FeedbackStorage {
29
+ entries: FeedbackEntry[];
30
+ lastUpdated: string;
31
+ version: string;
32
+ }
33
+ export declare function getMaxEntries(): number;
34
+ export declare function getFeedbackDir(): string;
35
+ export declare function getFeedbackFile(): string;
36
+ export declare function ensureFeedbackDir(): string;
37
+ export declare function loadFeedbackStorage(): FeedbackStorage;
38
+ export declare function saveFeedbackStorage(storage: FeedbackStorage): void;
39
+ export declare function generateFeedbackId(type: string, timestamp: string): string;
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ /**
3
+ * Shared feedback storage layer.
4
+ * Used by both the MCP feedback_submit handler and the future dashboard CRUD REST routes.
5
+ * All file I/O lives here — no duplication across consumers.
6
+ */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.getMaxEntries = getMaxEntries;
12
+ exports.getFeedbackDir = getFeedbackDir;
13
+ exports.getFeedbackFile = getFeedbackFile;
14
+ exports.ensureFeedbackDir = ensureFeedbackDir;
15
+ exports.loadFeedbackStorage = loadFeedbackStorage;
16
+ exports.saveFeedbackStorage = saveFeedbackStorage;
17
+ exports.generateFeedbackId = generateFeedbackId;
18
+ const logger_1 = require("./logger");
19
+ const runtimeConfig_1 = require("../config/runtimeConfig");
20
+ const fs_1 = __importDefault(require("fs"));
21
+ const path_1 = __importDefault(require("path"));
22
+ const crypto_1 = require("crypto");
23
+ function getMaxEntries() {
24
+ return (0, runtimeConfig_1.getRuntimeConfig)().feedback.maxEntries;
25
+ }
26
+ function getFeedbackDir() {
27
+ return (0, runtimeConfig_1.getRuntimeConfig)().feedback.dir;
28
+ }
29
+ function getFeedbackFile() {
30
+ return path_1.default.join(getFeedbackDir(), 'feedback-entries.json');
31
+ }
32
+ function ensureFeedbackDir() {
33
+ const dir = getFeedbackDir();
34
+ if (!fs_1.default.existsSync(dir)) {
35
+ try {
36
+ fs_1.default.mkdirSync(dir, { recursive: true });
37
+ }
38
+ catch (error) {
39
+ (0, logger_1.logError)('[feedback] Failed to create feedback directory', { error: String(error), dir });
40
+ }
41
+ }
42
+ return dir;
43
+ }
44
+ function loadFeedbackStorage() {
45
+ const file = getFeedbackFile();
46
+ ensureFeedbackDir();
47
+ try {
48
+ if (fs_1.default.existsSync(file)) {
49
+ const content = fs_1.default.readFileSync(file, 'utf8');
50
+ const parsed = JSON.parse(content);
51
+ if (!parsed.entries || !Array.isArray(parsed.entries)) {
52
+ throw new Error('Invalid feedback storage format');
53
+ }
54
+ return parsed;
55
+ }
56
+ }
57
+ catch (error) {
58
+ (0, logger_1.logWarn)('[feedback] Failed to load feedback storage, initializing empty', { error: String(error) });
59
+ }
60
+ return { entries: [], lastUpdated: new Date().toISOString(), version: '1.0.0' };
61
+ }
62
+ function saveFeedbackStorage(storage) {
63
+ const file = getFeedbackFile();
64
+ ensureFeedbackDir();
65
+ try {
66
+ const maxEntries = getMaxEntries();
67
+ const entries = storage.entries.length > maxEntries
68
+ ? [...storage.entries]
69
+ .sort((a, b) => b.timestamp.localeCompare(a.timestamp))
70
+ .slice(0, maxEntries)
71
+ : [...storage.entries];
72
+ const content = JSON.stringify({
73
+ ...storage,
74
+ entries,
75
+ lastUpdated: new Date().toISOString(),
76
+ }, null, 2);
77
+ fs_1.default.writeFileSync(file, content, 'utf8');
78
+ }
79
+ catch (error) {
80
+ (0, logger_1.logError)('[feedback] Failed to save feedback storage', error instanceof Error ? error : { error: String(error) });
81
+ throw error;
82
+ }
83
+ }
84
+ function generateFeedbackId(type, timestamp) {
85
+ const hash = (0, crypto_1.createHash)('sha256');
86
+ hash.update(`${type}-${timestamp}-${Math.random()}`);
87
+ return hash.digest('hex').substring(0, 16);
88
+ }