@jagilber-org/index-server 1.22.0 → 1.26.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 (189) hide show
  1. package/CHANGELOG.md +87 -2
  2. package/CODE_OF_CONDUCT.md +2 -0
  3. package/CONTRIBUTING.md +32 -2
  4. package/README.md +83 -20
  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 +173 -54
  15. package/dist/dashboard/client/css/admin.css +151 -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 +25 -6
  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 +267 -82
  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 +4 -4
  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 +11 -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/setup-wizard.mjs +781 -0
  183. package/server.json +1 -0
  184. package/dist/externalClientLib.d.ts +0 -1
  185. package/dist/externalClientLib.js +0 -2
  186. package/dist/portableClientWrapper.d.ts +0 -1
  187. package/dist/portableClientWrapper.js +0 -2
  188. package/dist/services/indexingService.d.ts +0 -1
  189. package/dist/services/indexingService.js +0 -2
@@ -19,6 +19,16 @@ const instruction_schema_json_1 = __importDefault(require("../../schemas/instruc
19
19
  const tracing_1 = require("./tracing");
20
20
  const runtimeConfig_1 = require("../config/runtimeConfig");
21
21
  const autoSplit_1 = require("./autoSplit");
22
+ const logger_js_1 = require("./logger.js");
23
+ function sleep(ms) {
24
+ return new Promise(resolve => setTimeout(resolve, ms));
25
+ }
26
+ function getRetryBackoffMs(baseBackoff, attempt) {
27
+ return baseBackoff * Math.pow(2, attempt - 1) + Math.floor(Math.random() * baseBackoff);
28
+ }
29
+ function isRetryableLoadError(error) {
30
+ return /empty file transient|unexpected end of json input|eprem|ebusy|eacces|enoent/i.test(error);
31
+ }
22
32
  class IndexLoader {
23
33
  baseDir;
24
34
  classifier;
@@ -32,9 +42,8 @@ class IndexLoader {
32
42
  * to momentary locks while another process is atomically renaming/writing.
33
43
  */
34
44
  readJsonWithRetry(file) {
35
- const { attempts, backoffMs } = (0, runtimeConfig_1.getRuntimeConfig)().index.readRetries;
45
+ const { attempts } = (0, runtimeConfig_1.getRuntimeConfig)().index.readRetries;
36
46
  const maxAttempts = Math.max(1, attempts);
37
- const baseBackoff = Math.max(1, backoffMs);
38
47
  let lastErr = null;
39
48
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
40
49
  try {
@@ -58,15 +67,26 @@ class IndexLoader {
58
67
  break;
59
68
  }
60
69
  lastErr = err;
61
- const sleep = baseBackoff * Math.pow(2, attempt - 1) + Math.floor(Math.random() * baseBackoff);
62
- const start = Date.now();
63
- while (Date.now() - start < sleep) { /* spin tiny backoff (< few ms) */ }
64
70
  }
65
71
  }
66
72
  if (lastErr)
67
73
  throw lastErr instanceof Error ? lastErr : new Error('readJsonWithRetry failed');
68
74
  return {}; // unreachable but satisfies typing
69
75
  }
76
+ async loadAsync() {
77
+ const { attempts, backoffMs } = (0, runtimeConfig_1.getRuntimeConfig)().index.readRetries;
78
+ const maxAttempts = Math.max(1, attempts);
79
+ const baseBackoff = Math.max(1, backoffMs);
80
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
81
+ const result = this.load();
82
+ const retryable = result.errors.length > 0 && result.errors.every(error => isRetryableLoadError(error.error));
83
+ if (!retryable || attempt === maxAttempts) {
84
+ return result;
85
+ }
86
+ await sleep(getRetryBackoffMs(baseBackoff, attempt));
87
+ }
88
+ return this.load();
89
+ }
70
90
  load() {
71
91
  const runtimeConfig = (0, runtimeConfig_1.getRuntimeConfig)();
72
92
  const IndexConfig = runtimeConfig.index;
@@ -126,7 +146,7 @@ class IndexLoader {
126
146
  }
127
147
  catch { /* ignore meta-schema registration issues */ }
128
148
  // Patch schema body maxLength from config before compiling (allows INDEX_SERVER_BODY_WARN_LENGTH override)
129
- const bodyMaxLen = IndexConfig.bodyWarnLength || 100000;
149
+ const bodyMaxLen = IndexConfig.bodyWarnLength || 50000;
130
150
  const autoSplit = IndexConfig.autoSplitOversized === true;
131
151
  const schemaCopy = JSON.parse(JSON.stringify(instruction_schema_json_1.default));
132
152
  try {
@@ -672,7 +692,7 @@ class IndexLoader {
672
692
  trace.push({ file: f, accepted: false, reason });
673
693
  // Log schema rejections at info level for operational visibility
674
694
  try {
675
- console.error(`[index:skip] ${f}: ${reason}`);
695
+ (0, logger_js_1.logError)(`[index:skip] ${f}: ${reason}`);
676
696
  }
677
697
  catch { /* ignore */ }
678
698
  if ((0, tracing_1.traceEnabled)(1)) {
@@ -698,7 +718,7 @@ class IndexLoader {
698
718
  if (trace)
699
719
  trace.push({ file: f, accepted: false, reason });
700
720
  try {
701
- console.error(`[index:skip] ${f}: classification: ${reason}`);
721
+ (0, logger_js_1.logError)(`[index:skip] ${f}: classification: ${reason}`);
702
722
  }
703
723
  catch { /* ignore */ }
704
724
  if ((0, tracing_1.traceEnabled)(1)) {
@@ -0,0 +1,39 @@
1
+ import { InstructionEntry } from '../models/instruction';
2
+ declare const INPUT_SCHEMA_REF = "index_add#input";
3
+ export interface InstructionValidationResult {
4
+ record: InstructionEntry;
5
+ validationErrors: string[];
6
+ hints: string[];
7
+ schemaRef: string;
8
+ }
9
+ export declare class InstructionValidationError extends Error {
10
+ readonly validationErrors: string[];
11
+ readonly hints: string[];
12
+ readonly schemaRef: string;
13
+ readonly code = "invalid_instruction";
14
+ constructor(validationErrors: string[], hints?: string[], schemaRef?: string);
15
+ }
16
+ export declare const INSTRUCTION_ID_MAX_LENGTH = 120;
17
+ export declare function validateInstructionInputSurface(entry: Record<string, unknown>): InstructionValidationResult;
18
+ export declare function validateInstructionRecord(entry: InstructionEntry): InstructionValidationResult;
19
+ export declare function assertValidInstructionRecord(entry: InstructionEntry): InstructionEntry;
20
+ export declare function isInstructionValidationError(error: unknown): error is InstructionValidationError;
21
+ export type LoadErrorCode = 'load_failed' | 'parse_failed' | 'unknown';
22
+ export interface SanitizedLoadError {
23
+ code: LoadErrorCode;
24
+ detail: string;
25
+ raw: string;
26
+ }
27
+ /**
28
+ * Strip absolute paths, Node fs error codes, quoted path arguments, and
29
+ * stack traces from a free-form error message. Used to keep client-facing
30
+ * error responses free of filesystem layout or internal details.
31
+ */
32
+ export declare function sanitizeErrorDetail(message: string): string;
33
+ /**
34
+ * Convert an arbitrary error from the existing-entry load path into a
35
+ * client-safe shape. The `raw` field is preserved for internal audit logging
36
+ * only; never echo it directly to clients.
37
+ */
38
+ export declare function sanitizeLoadError(err: unknown, kind?: LoadErrorCode): SanitizedLoadError;
39
+ export { INPUT_SCHEMA_REF as INSTRUCTION_INPUT_SCHEMA_REF };
@@ -0,0 +1,388 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.INSTRUCTION_INPUT_SCHEMA_REF = exports.INSTRUCTION_ID_MAX_LENGTH = exports.InstructionValidationError = void 0;
7
+ exports.validateInstructionInputSurface = validateInstructionInputSurface;
8
+ exports.validateInstructionRecord = validateInstructionRecord;
9
+ exports.assertValidInstructionRecord = assertValidInstructionRecord;
10
+ exports.isInstructionValidationError = isInstructionValidationError;
11
+ exports.sanitizeErrorDetail = sanitizeErrorDetail;
12
+ exports.sanitizeLoadError = sanitizeLoadError;
13
+ const ajv_1 = __importDefault(require("ajv"));
14
+ const ajv_formats_1 = __importDefault(require("ajv-formats"));
15
+ const json_schema_draft_07_json_1 = __importDefault(require("ajv/dist/refs/json-schema-draft-07.json"));
16
+ const schemas_1 = require("../schemas");
17
+ const classificationService_1 = require("./classificationService");
18
+ const INPUT_SCHEMA_REF = 'index_add#input';
19
+ exports.INSTRUCTION_INPUT_SCHEMA_REF = INPUT_SCHEMA_REF;
20
+ const REQUIRED_RECORD_KEYS = new Set([
21
+ 'id',
22
+ 'title',
23
+ 'body',
24
+ 'priority',
25
+ 'audience',
26
+ 'requirement',
27
+ 'categories',
28
+ 'sourceHash',
29
+ 'schemaVersion',
30
+ 'createdAt',
31
+ 'updatedAt',
32
+ 'version',
33
+ 'status',
34
+ 'owner',
35
+ 'priorityTier',
36
+ 'classification',
37
+ 'lastReviewedAt',
38
+ 'nextReviewDue',
39
+ 'changeLog',
40
+ 'semanticSummary',
41
+ ]);
42
+ const ALLOWED_INPUT_KEYS = new Set([
43
+ 'id',
44
+ 'title',
45
+ 'body',
46
+ 'rationale',
47
+ 'priority',
48
+ 'audience',
49
+ 'requirement',
50
+ 'categories',
51
+ 'primaryCategory',
52
+ 'deprecatedBy',
53
+ 'riskScore',
54
+ 'reviewIntervalDays',
55
+ 'version',
56
+ 'owner',
57
+ 'status',
58
+ 'priorityTier',
59
+ 'classification',
60
+ 'lastReviewedAt',
61
+ 'nextReviewDue',
62
+ 'changeLog',
63
+ 'semanticSummary',
64
+ 'contentType',
65
+ 'extensions',
66
+ 'supersedes',
67
+ 'createdByAgent',
68
+ 'sourceWorkspace',
69
+ 'mode',
70
+ 'lax',
71
+ ]);
72
+ const ajv = new ajv_1.default({ allErrors: true, strict: false });
73
+ (0, ajv_formats_1.default)(ajv);
74
+ try {
75
+ if (!ajv.getSchema('https://json-schema.org/draft-07/schema')) {
76
+ ajv.addMetaSchema(json_schema_draft_07_json_1.default, 'https://json-schema.org/draft-07/schema');
77
+ }
78
+ }
79
+ catch {
80
+ // Non-fatal; loader uses the same best-effort registration pattern.
81
+ }
82
+ const validateInstructionSchema = ajv.compile(JSON.parse(JSON.stringify(schemas_1.instructionEntry)));
83
+ class InstructionValidationError extends Error {
84
+ validationErrors;
85
+ hints;
86
+ schemaRef;
87
+ code = 'invalid_instruction';
88
+ constructor(validationErrors, hints = [], schemaRef = INPUT_SCHEMA_REF) {
89
+ super(`invalid_instruction: ${validationErrors.join('; ')}`);
90
+ this.validationErrors = validationErrors;
91
+ this.hints = hints;
92
+ this.schemaRef = schemaRef;
93
+ this.name = 'InstructionValidationError';
94
+ }
95
+ }
96
+ exports.InstructionValidationError = InstructionValidationError;
97
+ function normalizePath(path) {
98
+ return path || '/';
99
+ }
100
+ function formatAjvError(error) {
101
+ const instancePath = normalizePath(error.instancePath);
102
+ if (error.keyword === 'additionalProperties') {
103
+ const prop = error.params.additionalProperty;
104
+ return `${instancePath}: unexpected property "${prop}"`;
105
+ }
106
+ if (error.keyword === 'required') {
107
+ const prop = error.params.missingProperty;
108
+ return `${instancePath}: missing required property "${prop}"`;
109
+ }
110
+ if (error.keyword === 'enum') {
111
+ const allowed = Array.isArray(error.params.allowedValues)
112
+ ? (error.params.allowedValues ?? []).join(', ')
113
+ : 'allowed enum values';
114
+ return `${instancePath}: must be one of ${allowed}`;
115
+ }
116
+ if (error.keyword === 'type') {
117
+ const expected = error.params.type ?? 'the expected type';
118
+ return `${instancePath}: must be ${expected}`;
119
+ }
120
+ if (error.keyword === 'minLength')
121
+ return `${instancePath}: must not be empty`;
122
+ if (error.keyword === 'maxLength')
123
+ return `${instancePath}: exceeds the allowed maximum length`;
124
+ if (error.keyword === 'minimum')
125
+ return `${instancePath}: must be greater than or equal to ${error.params.comparison ?? 'the minimum'}`;
126
+ if (error.keyword === 'maximum')
127
+ return `${instancePath}: must be less than or equal to ${error.params.comparison ?? 'the maximum'}`;
128
+ return `${instancePath}: ${error.message ?? 'failed validation'}`;
129
+ }
130
+ function dedupe(items) {
131
+ return Array.from(new Set(items));
132
+ }
133
+ function buildHints(validationErrors) {
134
+ const hints = [
135
+ 'Instruction not added. Fix the listed validation errors and retry.',
136
+ 'Use the returned inputSchema as the authoritative contract for index_add.',
137
+ ];
138
+ if (validationErrors.some((msg) => msg.includes('missing required property') || msg.includes('missing required field'))) {
139
+ hints.push('Provide all required fields for strict add/import calls, especially id, title, and body.');
140
+ }
141
+ if (validationErrors.some((msg) => msg.includes('must be one of') || msg.includes('invalid value'))) {
142
+ hints.push('Use documented enum values for fields like audience, requirement, status, priorityTier, classification, and contentType.');
143
+ }
144
+ if (validationErrors.some((msg) => msg.includes('unexpected property'))) {
145
+ hints.push('Remove unsupported properties instead of sending fields that are not part of the instruction schema.');
146
+ }
147
+ if (validationErrors.some((msg) => msg.includes('/extensions') || msg.includes('extensions'))) {
148
+ hints.push('extensions must be a JSON object whose values are strings, numbers, booleans, arrays, or nested objects; null is not allowed.');
149
+ }
150
+ if (validationErrors.some((msg) => msg.includes('null is not allowed'))) {
151
+ hints.push('Replace null values with a valid value or omit the optional field entirely.');
152
+ }
153
+ return dedupe(hints);
154
+ }
155
+ function stripUndefinedAndOptionalNulls(value, key, depth = 0) {
156
+ if (Array.isArray(value)) {
157
+ return value.map((item) => stripUndefinedAndOptionalNulls(item, undefined, depth + 1)).filter((item) => item !== undefined);
158
+ }
159
+ if (!value || typeof value !== 'object')
160
+ return value;
161
+ const out = {};
162
+ for (const [childKey, childValue] of Object.entries(value)) {
163
+ if (childValue === undefined)
164
+ continue;
165
+ if (childValue === null && depth === 0 && !REQUIRED_RECORD_KEYS.has(childKey))
166
+ continue;
167
+ out[childKey] = stripUndefinedAndOptionalNulls(childValue, childKey, depth + 1); // lgtm[js/remote-property-injection] — childKey is own-property of caller-controlled entry; schema rejects unknown top-level keys downstream
168
+ }
169
+ if (key && Object.keys(out).length === 0 && !REQUIRED_RECORD_KEYS.has(key))
170
+ return undefined;
171
+ return out;
172
+ }
173
+ function applyWriteCompatibility(entry) {
174
+ const next = stripUndefinedAndOptionalNulls(entry);
175
+ if (next.status === 'active')
176
+ next.status = 'approved';
177
+ if (next.audience === undefined)
178
+ next.audience = 'all';
179
+ if (next.requirement === undefined)
180
+ next.requirement = 'optional';
181
+ if (typeof next.audience === 'string') {
182
+ const legacyAudienceMap = {
183
+ system: 'all',
184
+ developers: 'group',
185
+ developer: 'individual',
186
+ team: 'group',
187
+ teams: 'group',
188
+ users: 'group',
189
+ dev: 'individual',
190
+ devs: 'group',
191
+ testers: 'group',
192
+ administrators: 'group',
193
+ admins: 'group',
194
+ agents: 'group',
195
+ 'powershell script authors': 'group',
196
+ };
197
+ const lower = next.audience.toLowerCase();
198
+ if (legacyAudienceMap[next.audience])
199
+ next.audience = legacyAudienceMap[next.audience];
200
+ else if (legacyAudienceMap[lower])
201
+ next.audience = legacyAudienceMap[lower];
202
+ else if (/author|script\s+author/i.test(lower))
203
+ next.audience = 'individual';
204
+ }
205
+ if (typeof next.requirement === 'string') {
206
+ const legacyRequirementMap = {
207
+ MUST: 'mandatory',
208
+ SHOULD: 'recommended',
209
+ MAY: 'optional',
210
+ CRITICAL: 'critical',
211
+ OPTIONAL: 'optional',
212
+ MANDATORY: 'mandatory',
213
+ DEPRECATED: 'deprecated',
214
+ };
215
+ const upper = next.requirement.toUpperCase();
216
+ if (legacyRequirementMap[next.requirement])
217
+ next.requirement = legacyRequirementMap[next.requirement];
218
+ else if (legacyRequirementMap[upper])
219
+ next.requirement = legacyRequirementMap[upper];
220
+ }
221
+ if (typeof next.priority !== 'number' || next.priority < 1 || next.priority > 100)
222
+ next.priority = 50;
223
+ return next;
224
+ }
225
+ // Matches the upper bound declared in schemas/instruction.schema.json (id maxLength).
226
+ exports.INSTRUCTION_ID_MAX_LENGTH = 120;
227
+ function findIllegalControlChar(id) {
228
+ for (let i = 0; i < id.length; i++) {
229
+ const code = id.charCodeAt(i);
230
+ // Reject ASCII control characters (incl. NUL 0x00) and DEL (0x7F).
231
+ if (code < 0x20 || code === 0x7f) {
232
+ const display = `\\x${code.toString(16).padStart(2, '0')}`;
233
+ return { display, code };
234
+ }
235
+ }
236
+ return undefined;
237
+ }
238
+ function validateIdSurface(id) {
239
+ if (typeof id !== 'string' || !id.trim())
240
+ return ['id: missing required field'];
241
+ const illegal = findIllegalControlChar(id);
242
+ if (illegal) {
243
+ return [`id: contains illegal control character (${illegal.display}) — id-illegal-character`];
244
+ }
245
+ if (id.length > exports.INSTRUCTION_ID_MAX_LENGTH) {
246
+ return [`id: exceeds maximum length of ${exports.INSTRUCTION_ID_MAX_LENGTH} characters (id-too-long)`];
247
+ }
248
+ if (id.includes('..') || id.includes('/') || id.includes('\\') || /[:*?"<>|]/.test(id)) {
249
+ return ['id: must be a safe instruction id without path traversal or path separators'];
250
+ }
251
+ return [];
252
+ }
253
+ // Typed-field shape checks. These run for both strict and lax callers so that lax mode
254
+ // fills defaults for *missing* fields but never silently coerces wrong-typed inputs.
255
+ function validateTypedInputShape(entry) {
256
+ const errs = [];
257
+ if (entry.priority !== undefined && typeof entry.priority !== 'number') {
258
+ errs.push(`/priority: must be a number, received ${typeof entry.priority}`);
259
+ }
260
+ if (entry.categories !== undefined && !Array.isArray(entry.categories)) {
261
+ errs.push(`/categories: must be an array of strings, received ${typeof entry.categories}`);
262
+ }
263
+ if (entry.audience !== undefined && typeof entry.audience !== 'string') {
264
+ errs.push(`/audience: must be a string, received ${typeof entry.audience}`);
265
+ }
266
+ if (entry.requirement !== undefined && typeof entry.requirement !== 'string') {
267
+ errs.push(`/requirement: must be a string, received ${typeof entry.requirement}`);
268
+ }
269
+ if (entry.title !== undefined && typeof entry.title !== 'string') {
270
+ errs.push(`/title: must be a string, received ${typeof entry.title}`);
271
+ }
272
+ if (entry.body !== undefined && typeof entry.body !== 'string') {
273
+ errs.push(`/body: must be a string, received ${typeof entry.body}`);
274
+ }
275
+ if (entry.changeLog !== undefined && !Array.isArray(entry.changeLog)) {
276
+ errs.push(`/changeLog: must be an array, received ${typeof entry.changeLog}`);
277
+ }
278
+ if (entry.extensions !== undefined && (typeof entry.extensions !== 'object' || Array.isArray(entry.extensions))) {
279
+ errs.push(`/extensions: must be an object, received ${Array.isArray(entry.extensions) ? 'array' : typeof entry.extensions}`);
280
+ }
281
+ return errs;
282
+ }
283
+ function validateInstructionInputSurface(entry) {
284
+ const validationErrors = [];
285
+ validationErrors.push(...validateIdSurface(entry.id));
286
+ for (const key of Object.keys(entry)) {
287
+ if (!ALLOWED_INPUT_KEYS.has(key)) {
288
+ validationErrors.push(`/: unexpected property "${key}"`);
289
+ continue;
290
+ }
291
+ if (entry[key] === null)
292
+ validationErrors.push(`/${key}: null is not allowed`);
293
+ }
294
+ validationErrors.push(...validateTypedInputShape(entry));
295
+ return {
296
+ record: entry,
297
+ validationErrors: dedupe(validationErrors),
298
+ hints: buildHints(validationErrors),
299
+ schemaRef: INPUT_SCHEMA_REF,
300
+ };
301
+ }
302
+ function validateInstructionRecord(entry) {
303
+ const record = applyWriteCompatibility(entry);
304
+ const validationErrors = [];
305
+ if (record.status !== undefined && !['draft', 'review', 'approved', 'deprecated'].includes(record.status)) {
306
+ validationErrors.push(`/status: invalid value "${String(record.status)}"`);
307
+ }
308
+ if (record.priorityTier !== undefined && !['P1', 'P2', 'P3', 'P4'].includes(record.priorityTier)) {
309
+ validationErrors.push(`/priorityTier: invalid value "${String(record.priorityTier)}"`);
310
+ }
311
+ if (record.classification !== undefined && !['public', 'internal', 'restricted'].includes(record.classification)) {
312
+ validationErrors.push(`/classification: invalid value "${String(record.classification)}"`);
313
+ }
314
+ if (record.contentType !== undefined && !['instruction', 'template', 'chat-session', 'reference', 'example', 'agent'].includes(record.contentType)) {
315
+ validationErrors.push(`/contentType: invalid value "${String(record.contentType)}"`);
316
+ }
317
+ if (!validateInstructionSchema(record)) {
318
+ validationErrors.push(...(validateInstructionSchema.errors ?? []).map(formatAjvError));
319
+ }
320
+ const classifierIssues = new classificationService_1.ClassificationService().validate(record);
321
+ validationErrors.push(...classifierIssues.map((issue) => `/: ${issue}`));
322
+ if (typeof record.title === 'string' && !record.title.trim())
323
+ validationErrors.push('/title: must not be empty');
324
+ if (typeof record.body === 'string' && !record.body.trim())
325
+ validationErrors.push('/body: must not be empty');
326
+ return {
327
+ record,
328
+ validationErrors: dedupe(validationErrors),
329
+ hints: buildHints(validationErrors),
330
+ schemaRef: INPUT_SCHEMA_REF,
331
+ };
332
+ }
333
+ function assertValidInstructionRecord(entry) {
334
+ const validation = validateInstructionRecord(entry);
335
+ if (validation.validationErrors.length) {
336
+ throw new InstructionValidationError(validation.validationErrors, validation.hints, validation.schemaRef);
337
+ }
338
+ return validation.record;
339
+ }
340
+ function isInstructionValidationError(error) {
341
+ return error instanceof InstructionValidationError
342
+ || (typeof error === 'object'
343
+ && error !== null
344
+ && 'code' in error
345
+ && error.code === 'invalid_instruction'
346
+ && Array.isArray(error.validationErrors));
347
+ }
348
+ const NODE_FS_ERROR_CODES = /\b(ENOENT|EACCES|EEXIST|EISDIR|ENOTDIR|EPERM|EBUSY|EMFILE|ENFILE|EROFS|ENOSPC|EAGAIN|EFAULT|EINVAL|EIO|ELOOP)\b[:,]?\s*/g;
349
+ /**
350
+ * Strip absolute paths, Node fs error codes, quoted path arguments, and
351
+ * stack traces from a free-form error message. Used to keep client-facing
352
+ * error responses free of filesystem layout or internal details.
353
+ */
354
+ function sanitizeErrorDetail(message) {
355
+ if (!message)
356
+ return '';
357
+ // Truncate to first line — drops stack traces.
358
+ let s = String(message).split('\n')[0];
359
+ // Quoted path arguments (single and double quotes), e.g. open 'C:\\x.json'.
360
+ s = s.replace(/'[^']*[\\/][^']*'/g, "'<redacted-path>'");
361
+ s = s.replace(/"[^"]*[\\/][^"]*"/g, '"<redacted-path>"');
362
+ // Windows absolute paths.
363
+ s = s.replace(/[A-Za-z]:\\[^\s'"`]+/g, '<redacted-path>');
364
+ // Unix absolute paths with at least two segments.
365
+ s = s.replace(/\/(?:[^\s/'"`]+\/)+[^\s/'"`]+/g, '<redacted-path>');
366
+ // Node fs error codes (after path stripping so we don't break boundaries).
367
+ s = s.replace(NODE_FS_ERROR_CODES, '');
368
+ // Collapse whitespace and stray punctuation.
369
+ s = s.replace(/\s+/g, ' ').replace(/^[\s:,;-]+/, '').replace(/[\s:,;-]+$/, '').trim();
370
+ return s;
371
+ }
372
+ /**
373
+ * Convert an arbitrary error from the existing-entry load path into a
374
+ * client-safe shape. The `raw` field is preserved for internal audit logging
375
+ * only; never echo it directly to clients.
376
+ */
377
+ function sanitizeLoadError(err, kind = 'load_failed') {
378
+ const raw = err instanceof Error ? (err.message ?? '') : (typeof err === 'string' ? err : '');
379
+ let detail = sanitizeErrorDetail(raw);
380
+ if (!detail) {
381
+ detail = kind === 'parse_failed'
382
+ ? 'invalid JSON in existing entry'
383
+ : kind === 'load_failed'
384
+ ? 'unable to read existing entry'
385
+ : 'unknown load error';
386
+ }
387
+ return { code: kind, detail, raw };
388
+ }
@@ -164,11 +164,11 @@ function isMutationEnabled() {
164
164
  if (mutationMethods.has(target) && !isMutationEnabled()) {
165
165
  // Dispatcher design intent: allow mutation-style actions even when direct mutation tools
166
166
  // are disabled. The previous logic incorrectly blocked these calls, causing silent timeouts
167
- // in tests expecting dispatcher add to succeed without INDEX_SERVER_MUTATION=1.
167
+ // in tests expecting dispatcher add to succeed even when direct mutation calls were forced off.
168
168
  // We now log (if verbose) and proceed instead of throwing a semantic error.
169
169
  try {
170
170
  if ((0, runtimeConfig_1.getRuntimeConfig)().logging.verbose)
171
- process.stderr.write(`[dispatcher] mutation_allowed_via_dispatcher action=${action} target=${target} (INDEX_SERVER_MUTATION not set)\n`);
171
+ process.stderr.write(`[dispatcher] mutation_allowed_via_dispatcher action=${action} target=${target} (direct mutation override disabled)\n`);
172
172
  }
173
173
  catch { /* ignore */ }
174
174
  }
@@ -192,7 +192,7 @@ function isMutationEnabled() {
192
192
  // because the dispatch schema cannot express nested 'entry' wrappers.
193
193
  // When 'entry' is absent but 'id' is present, assemble the entry from flat params.
194
194
  if (action === 'add' && !rest.entry && typeof rest.id === 'string') {
195
- const entryFields = ['id', 'body', 'title', 'rationale', 'priority', 'audience', 'requirement', 'categories', 'deprecatedBy', 'riskScore', 'version', 'owner', 'status', 'priorityTier', 'classification', 'lastReviewedAt', 'nextReviewDue', 'semanticSummary', 'changeLog', 'extensions'];
195
+ const entryFields = ['id', 'body', 'title', 'rationale', 'priority', 'audience', 'requirement', 'categories', 'deprecatedBy', 'riskScore', 'version', 'owner', 'status', 'priorityTier', 'classification', 'lastReviewedAt', 'nextReviewDue', 'semanticSummary', 'changeLog', 'contentType', 'extensions'];
196
196
  const entry = {};
197
197
  for (const k of entryFields) {
198
198
  if (rest[k] !== undefined) {
@@ -204,7 +204,7 @@ function isMutationEnabled() {
204
204
  }
205
205
  void _ignoredAction; // explicitly ignore for lint
206
206
  // Mark invocation origin so guard() can allow dispatcher-mediated mutations even if
207
- // INDEX_SERVER_MUTATION is not globally enabled.
207
+ // direct mutation tools were explicitly disabled via runtime override.
208
208
  rest._viaDispatcher = true;
209
209
  const hStart = timing ? Date.now() : 0;
210
210
  // Gating: block mutation targets if bootstrap confirmation required or reference mode active.
@@ -0,0 +1,15 @@
1
+ export interface DiskValidationResult {
2
+ valid: boolean;
3
+ errors?: string[];
4
+ }
5
+ /**
6
+ * Validate an instruction record against the loader JSON schema — the same
7
+ * schema used by IndexLoader when loading entries from disk.
8
+ *
9
+ * Call this on the exact object about to be serialized with JSON.stringify()
10
+ * and written to disk. If it fails, the entry WILL be silently skipped on
11
+ * the next reload.
12
+ */
13
+ export declare function validateForDisk(record: unknown): DiskValidationResult;
14
+ /** Returns the set of property names allowed by the loader JSON schema. */
15
+ export declare function getSchemaPropertyNames(): Set<string>;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.validateForDisk = validateForDisk;
7
+ exports.getSchemaPropertyNames = getSchemaPropertyNames;
8
+ /**
9
+ * Shared loader-schema validator — uses the SAME JSON schema the IndexLoader
10
+ * uses at reload time, compiled once and cached at module scope.
11
+ *
12
+ * Every instruction write path MUST validate through this before persisting
13
+ * to disk. This is the single source of truth that prevents schema drift
14
+ * between write-time and load-time validation from silently dropping entries.
15
+ */
16
+ const ajv_1 = __importDefault(require("ajv"));
17
+ const ajv_formats_1 = __importDefault(require("ajv-formats"));
18
+ const json_schema_draft_07_json_1 = __importDefault(require("ajv/dist/refs/json-schema-draft-07.json"));
19
+ const instruction_schema_json_1 = __importDefault(require("../../schemas/instruction.schema.json"));
20
+ const runtimeConfig_1 = require("../config/runtimeConfig");
21
+ const ajv = new ajv_1.default({ strict: false, allErrors: true });
22
+ (0, ajv_formats_1.default)(ajv);
23
+ // Register draft-07 meta schema under https id (mirrors IndexLoader behavior)
24
+ try {
25
+ const httpsIdNoHash = 'https://json-schema.org/draft-07/schema';
26
+ const httpsIdHash = 'https://json-schema.org/draft-07/schema#';
27
+ if (!ajv.getSchema(httpsIdNoHash))
28
+ ajv.addMetaSchema({ ...json_schema_draft_07_json_1.default, $id: httpsIdNoHash });
29
+ if (!ajv.getSchema(httpsIdHash))
30
+ ajv.addMetaSchema({ ...json_schema_draft_07_json_1.default, $id: httpsIdHash });
31
+ }
32
+ catch { /* ignore meta-schema registration issues */ }
33
+ // Patch body maxLength from config (mirrors IndexLoader behavior)
34
+ const schemaCopy = JSON.parse(JSON.stringify(instruction_schema_json_1.default));
35
+ try {
36
+ const bodyMaxLen = (0, runtimeConfig_1.getRuntimeConfig)().index?.bodyWarnLength || 50000;
37
+ const props = schemaCopy.properties;
38
+ if (props?.body)
39
+ props.body.maxLength = bodyMaxLen;
40
+ }
41
+ catch (err) {
42
+ // If the bodyMaxLength patch fails the validator falls back to whatever
43
+ // limit ships in the on-disk schema, which can mismatch the configured
44
+ // INDEX_SERVER_BODY_WARN_LENGTH and silently accept/reject entries.
45
+ // Surface the warning so operators can see the divergence.
46
+ // eslint-disable-next-line no-console
47
+ console.warn('[loaderSchemaValidator] failed to patch schema bodyMaxLength from runtime config:', err?.message || err);
48
+ }
49
+ const validate = ajv.compile(schemaCopy);
50
+ /**
51
+ * Validate an instruction record against the loader JSON schema — the same
52
+ * schema used by IndexLoader when loading entries from disk.
53
+ *
54
+ * Call this on the exact object about to be serialized with JSON.stringify()
55
+ * and written to disk. If it fails, the entry WILL be silently skipped on
56
+ * the next reload.
57
+ */
58
+ function validateForDisk(record) {
59
+ const valid = validate(record);
60
+ if (valid)
61
+ return { valid: true };
62
+ const errors = validate.errors?.map(e => `${e.instancePath || '(root)'} ${e.message}${e.params ? ' ' + JSON.stringify(e.params) : ''}`) ?? ['unknown validation error'];
63
+ return { valid: false, errors };
64
+ }
65
+ /** Returns the set of property names allowed by the loader JSON schema. */
66
+ function getSchemaPropertyNames() {
67
+ const props = instruction_schema_json_1.default.properties;
68
+ return props ? new Set(Object.keys(props)) : new Set();
69
+ }
@@ -10,6 +10,7 @@ const crypto_1 = __importDefault(require("crypto"));
10
10
  const fs_1 = __importDefault(require("fs"));
11
11
  const path_1 = __importDefault(require("path"));
12
12
  const runtimeConfig_1 = require("../config/runtimeConfig");
13
+ const mcpLogBridge_1 = require("./mcpLogBridge");
13
14
  /** Numeric priority for log level filtering (lower = more verbose). */
14
15
  const LEVEL_PRIORITY = {
15
16
  TRACE: 0,
@@ -177,8 +178,16 @@ function emit(rec) {
177
178
  if (rec.correlationId)
178
179
  out.correlationId = rec.correlationId;
179
180
  const logLine = JSON.stringify(out);
180
- // Always log to stderr for VS Code output panel
181
- console.error(logLine);
181
+ // Route through MCP protocol notifications/message when available.
182
+ // This gives VS Code correct severity (info/debug/warning/error) instead
183
+ // of tagging every line as [warning] [server stderr].
184
+ if ((0, mcpLogBridge_1.isMcpLogBridgeActive)()) {
185
+ (0, mcpLogBridge_1.sendMcpLog)(rec.level, logLine);
186
+ }
187
+ else {
188
+ // Pre-handshake: fall back to stderr (intercepted and buffered by McpStdioLogger)
189
+ console.error(logLine);
190
+ }
182
191
  // Also log to file if configured and available
183
192
  if (logFileHandle && !logFileHandle.destroyed) {
184
193
  try {