@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
@@ -15,11 +15,14 @@ const schemaVersion_1 = require("../../versioning/schemaVersion");
15
15
  const categoryRules_1 = require("../categoryRules");
16
16
  const canonical_1 = require("../canonical");
17
17
  const instructions_shared_1 = require("./instructions.shared");
18
+ const loaderSchemaValidator_1 = require("../loaderSchemaValidator");
19
+ const instructionRecordValidation_1 = require("../instructionRecordValidation");
18
20
  (0, registry_1.registerHandler)('index_enrich', (0, instructions_shared_1.guard)('index_enrich', () => {
19
21
  const st = (0, indexContext_1.ensureLoaded)();
20
22
  let rewritten = 0;
21
23
  const updated = [];
22
24
  const skipped = [];
25
+ const errors = [];
23
26
  for (const e of st.list) {
24
27
  // Use in-memory state as the raw record (works for both SQLite and JSON backends)
25
28
  const raw = { ...e };
@@ -27,7 +30,7 @@ const instructions_shared_1 = require("./instructions.shared");
27
30
  let needs = false;
28
31
  const nowIso = new Date().toISOString();
29
32
  if (!(typeof raw.sourceHash === 'string' && raw.sourceHash.length > 0)) {
30
- raw.sourceHash = e.sourceHash || crypto_1.default.createHash('sha256').update(String(e.body || ''), 'utf8').digest('hex');
33
+ raw.sourceHash = e.sourceHash || (0, instructions_shared_1.computeSourceHash)(String(e.body || ''));
31
34
  needs = true;
32
35
  }
33
36
  if (typeof raw.createdAt === 'string' && raw.createdAt.length === 0) {
@@ -109,7 +112,11 @@ const instructions_shared_1 = require("./instructions.shared");
109
112
  skipped.push(e.id);
110
113
  }
111
114
  }
112
- catch { /* ignore */ }
115
+ catch (err) {
116
+ const detail = err.message || 'unknown';
117
+ errors.push({ id: e.id, error: detail });
118
+ (0, auditLog_1.logAudit)('enrich_entry_error', e.id, { error: detail });
119
+ }
113
120
  }
114
121
  if (rewritten) {
115
122
  (0, indexContext_1.touchIndexVersion)();
@@ -117,8 +124,10 @@ const instructions_shared_1 = require("./instructions.shared");
117
124
  (0, indexContext_1.ensureLoaded)();
118
125
  }
119
126
  const resp = { rewritten, updated, skipped };
127
+ if (errors.length)
128
+ resp.errors = errors;
120
129
  if (rewritten) {
121
- (0, auditLog_1.logAudit)('enrich', updated, { rewritten, skipped: skipped.length });
130
+ (0, auditLog_1.logAudit)('enrich', updated, { rewritten, skipped: skipped.length, errors: errors.length });
122
131
  (0, manifestManager_1.attemptManifestUpdate)();
123
132
  }
124
133
  return resp;
@@ -127,29 +136,98 @@ const instructions_shared_1 = require("./instructions.shared");
127
136
  const st = (0, indexContext_1.ensureLoaded)();
128
137
  const toFix = [];
129
138
  for (const e of st.list) {
130
- const actual = crypto_1.default.createHash('sha256').update(e.body, 'utf8').digest('hex');
139
+ const actual = (0, instructions_shared_1.computeSourceHash)(e.body);
131
140
  if (actual !== e.sourceHash)
132
141
  toFix.push({ entry: e, actual });
133
142
  }
134
- if (!toFix.length)
135
- return { repaired: 0, updated: [] };
136
143
  const repaired = [];
144
+ const errors = [];
137
145
  for (const { entry, actual } of toFix) {
138
146
  try {
139
147
  const updated = { ...entry, sourceHash: actual, updatedAt: new Date().toISOString() };
140
148
  (0, indexContext_1.writeEntry)(updated);
141
149
  repaired.push(entry.id);
142
150
  }
143
- catch { /* ignore */ }
151
+ catch (err) {
152
+ const detail = err.message || 'unknown';
153
+ errors.push({ id: entry.id, error: detail });
154
+ (0, auditLog_1.logAudit)('repair_entry_error', entry.id, { error: detail });
155
+ }
144
156
  }
145
- if (repaired.length) {
157
+ // Repair skipped files: scan disk for .json files not in the loaded index (#207)
158
+ const skippedRepaired = [];
159
+ const skippedErrors = [];
160
+ try {
161
+ const dir = (0, indexContext_1.getInstructionsDir)();
162
+ const diskFiles = fs_1.default.readdirSync(dir).filter(f => f.endsWith('.json') && !f.startsWith('_'));
163
+ // Use the compiled-in schema property set so disk-resident vs static-import
164
+ // schemas can never diverge (review #211 finding 9).
165
+ const schemaProps = (0, loaderSchemaValidator_1.getSchemaPropertyNames)();
166
+ for (const file of diskFiles) {
167
+ const id = file.replace(/\.json$/, '');
168
+ if (st.byId.has(id))
169
+ continue; // already loaded, not skipped
170
+ const filePath = path_1.default.join(dir, file);
171
+ try {
172
+ const raw = JSON.parse(fs_1.default.readFileSync(filePath, 'utf8'));
173
+ if (!raw.id || !raw.body) {
174
+ skippedErrors.push({ id, error: 'missing required fields (id or body)' });
175
+ continue;
176
+ }
177
+ // Strip properties not in the JSON schema
178
+ if (schemaProps.size) {
179
+ const keys = Object.keys(raw);
180
+ let stripped = false;
181
+ for (const key of keys) {
182
+ if (!schemaProps.has(key)) {
183
+ delete raw[key];
184
+ stripped = true;
185
+ }
186
+ }
187
+ if (stripped) {
188
+ raw.sourceHash = (0, instructions_shared_1.computeSourceHash)(String(raw.body));
189
+ raw.updatedAt = new Date().toISOString();
190
+ // DI-4: mirror the loader's migration step before validating
191
+ // against the loader schema, matching writeEntry/writeEntryAsync.
192
+ // Without this, a stripped record carrying a legacy schemaVersion
193
+ // would fail validateForDisk even though the loader would have
194
+ // migrated it transparently on read.
195
+ (0, schemaVersion_1.migrateInstructionRecord)(raw);
196
+ // Gate the write through the same loader-schema validator the
197
+ // primary writeEntry path uses (review #211 finding 3). If the
198
+ // stripped record still fails validation, skip the write rather
199
+ // than persisting a record that the next reload will silently
200
+ // discard.
201
+ const diskCheck = (0, loaderSchemaValidator_1.validateForDisk)(raw);
202
+ if (!diskCheck.valid) {
203
+ skippedErrors.push({ id, error: (0, instructionRecordValidation_1.sanitizeErrorDetail)(`schema validation failed: ${(diskCheck.errors || []).join('; ')}`) || 'schema validation failed' });
204
+ continue;
205
+ }
206
+ fs_1.default.writeFileSync(filePath, JSON.stringify(raw, null, 2) + '\n', 'utf8');
207
+ skippedRepaired.push(id);
208
+ }
209
+ }
210
+ }
211
+ catch (err) {
212
+ // Sanitize the raw error before returning to the client (review #211 finding 6).
213
+ skippedErrors.push({ id, error: (0, instructionRecordValidation_1.sanitizeErrorDetail)(err.message) || 'unknown' });
214
+ }
215
+ }
216
+ }
217
+ catch (err) {
218
+ errors.push({ id: '__disk_scan__', error: (0, instructionRecordValidation_1.sanitizeErrorDetail)(`skipped-file scan failed: ${err.message || 'unknown'}`) || 'skipped-file scan failed' });
219
+ }
220
+ const allRepaired = [...repaired, ...skippedRepaired];
221
+ if (allRepaired.length) {
146
222
  (0, indexContext_1.touchIndexVersion)();
147
223
  (0, indexContext_1.invalidate)();
148
224
  (0, indexContext_1.ensureLoaded)();
149
225
  }
150
- const resp = { repaired: repaired.length, updated: repaired };
151
- if (repaired.length) {
152
- (0, auditLog_1.logAudit)('repair', repaired, { repaired: repaired.length });
226
+ const resp = {
227
+ repaired: allRepaired.length, updated: repaired, skippedRepaired, errors: [...errors, ...skippedErrors]
228
+ };
229
+ if (allRepaired.length) {
230
+ (0, auditLog_1.logAudit)('repair', allRepaired, { repaired: allRepaired.length, skippedRepaired: skippedRepaired.length, errors: errors.length + skippedErrors.length });
153
231
  (0, manifestManager_1.attemptManifestUpdate)();
154
232
  }
155
233
  return resp;
@@ -222,12 +300,8 @@ const instructions_shared_1 = require("./instructions.shared");
222
300
  }
223
301
  }
224
302
  }
225
- const isJunkCategory = (cat) => /^\d/.test(cat) || cat.length <= 1 || /^case-\d{6,}$/.test(cat);
226
303
  for (const e of byId.values()) {
227
- let normCats = Array.from(new Set((e.categories || []).filter(c => typeof c === 'string').map(c => c.toLowerCase())));
228
- normCats = normCats.filter(c => !isJunkCategory(c));
229
- normCats = normCats.filter(cat => !(cat.endsWith('s') && normCats.includes(cat.slice(0, -1))));
230
- normCats = normCats.sort();
304
+ const normCats = (0, instructions_shared_1.normalizeCategories)(e.categories || []);
231
305
  if (JSON.stringify(normCats) !== JSON.stringify(e.categories)) {
232
306
  e.categories = normCats;
233
307
  normalizedCategories++;
@@ -239,7 +313,7 @@ const instructions_shared_1 = require("./instructions.shared");
239
313
  if (mergeDuplicates) {
240
314
  const groups = new Map();
241
315
  for (const e of byId.values()) {
242
- const key = e.sourceHash || crypto_1.default.createHash('sha256').update(e.body, 'utf8').digest('hex');
316
+ const key = e.sourceHash || (0, instructions_shared_1.computeSourceHash)(e.body);
243
317
  const arr = groups.get(key) || [];
244
318
  arr.push(e);
245
319
  groups.set(key, arr);
@@ -316,7 +390,9 @@ const instructions_shared_1 = require("./instructions.shared");
316
390
  }
317
391
  }
318
392
  }
319
- catch { /* ignore */ }
393
+ catch (err) {
394
+ notes.push(`purgeLegacyScopes-failed:${e.id}:${err.message || String(err)}`);
395
+ }
320
396
  }
321
397
  if (dryRun && purgedScopes)
322
398
  notes.push(`would-purge:${purgedScopes}`);
@@ -328,10 +404,12 @@ const instructions_shared_1 = require("./instructions.shared");
328
404
  const derived = (0, categoryRules_1.deriveCategory)(e.id);
329
405
  if (derived === 'Other')
330
406
  continue;
331
- e.primaryCategory = derived.toLowerCase();
332
- const lc = derived.toLowerCase();
333
- if (!e.categories.includes(lc)) {
334
- e.categories = [...e.categories, lc].sort();
407
+ const slug = (0, categoryRules_1.slugifyCategory)(derived);
408
+ if (!slug)
409
+ continue;
410
+ e.primaryCategory = slug;
411
+ if (!e.categories.includes(slug)) {
412
+ e.categories = [...e.categories, slug].sort();
335
413
  }
336
414
  e.updatedAt = new Date().toISOString();
337
415
  remappedCategories++;
@@ -341,7 +419,7 @@ const instructions_shared_1 = require("./instructions.shared");
341
419
  {
342
420
  for (const e of byId.values()) {
343
421
  const storedHash = e.sourceHash || '';
344
- const actualHash = crypto_1.default.createHash('sha256').update(e.body, 'utf8').digest('hex');
422
+ const actualHash = (0, instructions_shared_1.computeSourceHash)(e.body);
345
423
  if (storedHash !== actualHash) {
346
424
  e.sourceHash = actualHash;
347
425
  repairedHashes++;
@@ -351,6 +429,7 @@ const instructions_shared_1 = require("./instructions.shared");
351
429
  }
352
430
  }
353
431
  deprecatedRemoved = toRemove.length;
432
+ const errors = [];
354
433
  if (!dryRun) {
355
434
  for (const id of toRemove) {
356
435
  byId.delete(id);
@@ -364,7 +443,10 @@ const instructions_shared_1 = require("./instructions.shared");
364
443
  filesRewritten++;
365
444
  }
366
445
  catch (err) {
367
- notes.push(`write-failed:${id}:${err.message}`);
446
+ const detail = err.message || String(err);
447
+ errors.push({ id, error: `write-failed: ${detail}` });
448
+ notes.push(`write-failed:${id}:${detail}`);
449
+ (0, auditLog_1.logAudit)('groom_entry_error', id, { error: detail, operation: 'write' });
368
450
  }
369
451
  }
370
452
  for (const id of toRemove) {
@@ -372,7 +454,10 @@ const instructions_shared_1 = require("./instructions.shared");
372
454
  (0, indexContext_1.removeEntry)(id);
373
455
  }
374
456
  catch (err) {
375
- notes.push(`delete-failed:${id}:${err.message}`);
457
+ const detail = err.message || String(err);
458
+ errors.push({ id, error: `delete-failed: ${detail}` });
459
+ notes.push(`delete-failed:${id}:${detail}`);
460
+ (0, auditLog_1.logAudit)('groom_entry_error', id, { error: detail, operation: 'delete' });
376
461
  }
377
462
  }
378
463
  if (updated.size || toRemove.length) {
@@ -389,8 +474,10 @@ const instructions_shared_1 = require("./instructions.shared");
389
474
  }
390
475
  const stAfter = (0, indexContext_1.ensureLoaded)();
391
476
  const resp = { previousHash, hash: stAfter.hash, scanned, repairedHashes, normalizedCategories, deprecatedRemoved, duplicatesMerged, signalApplied, filesRewritten, purgedScopes, migrated, remappedCategories, dryRun, notes };
477
+ if (errors.length)
478
+ resp.errors = errors;
392
479
  if (!dryRun && (repairedHashes || normalizedCategories || deprecatedRemoved || duplicatesMerged || signalApplied || filesRewritten || purgedScopes || migrated || remappedCategories)) {
393
- (0, auditLog_1.logAudit)('groom', undefined, { repairedHashes, normalizedCategories, deprecatedRemoved, duplicatesMerged, signalApplied, filesRewritten, purgedScopes, migrated, remappedCategories });
480
+ (0, auditLog_1.logAudit)('groom', undefined, { repairedHashes, normalizedCategories, deprecatedRemoved, duplicatesMerged, signalApplied, filesRewritten, purgedScopes, migrated, remappedCategories, errors: errors.length });
394
481
  (0, manifestManager_1.attemptManifestUpdate)();
395
482
  }
396
483
  return resp;
@@ -409,6 +496,7 @@ const instructions_shared_1 = require("./instructions.shared");
409
496
  } });
410
497
  let scanned = 0, changed = 0, fixedHash = 0, fixedVersion = 0, fixedTier = 0, addedTimestamps = 0, addedContentType = 0;
411
498
  const updatedIds = [];
499
+ const errors = [];
412
500
  const scannedIds = new Set();
413
501
  const SEMVER = /^\d+\.\d+\.\d+(?:[-+].*)?$/;
414
502
  for (const dir of dirs) {
@@ -486,7 +574,8 @@ const instructions_shared_1 = require("./instructions.shared");
486
574
  try {
487
575
  (0, indexContext_1.writeEntry)(rec);
488
576
  }
489
- catch {
577
+ catch (err) {
578
+ errors.push({ id: path_1.default.basename(full, '.json'), error: `write-failed: ${err.message || String(err)}` });
490
579
  continue;
491
580
  }
492
581
  }
@@ -548,7 +637,8 @@ const instructions_shared_1 = require("./instructions.shared");
548
637
  try {
549
638
  (0, indexContext_1.writeEntry)(rec);
550
639
  }
551
- catch {
640
+ catch (err) {
641
+ errors.push({ id: entry.id, error: `write-failed: ${err.message || String(err)}` });
552
642
  continue;
553
643
  }
554
644
  }
@@ -563,13 +653,20 @@ const instructions_shared_1 = require("./instructions.shared");
563
653
  (0, indexContext_1.invalidate)();
564
654
  (0, indexContext_1.ensureLoaded)();
565
655
  }
566
- catch { /* ignore */ }
656
+ catch (err) {
657
+ (0, auditLog_1.logAudit)('normalize_reload_error', undefined, { error: err.message });
658
+ }
567
659
  try {
568
660
  (0, manifestManager_1.attemptManifestUpdate)();
569
661
  }
570
- catch { /* ignore */ }
662
+ catch (err) {
663
+ (0, auditLog_1.logAudit)('normalize_manifest_error', undefined, { error: err.message });
664
+ }
571
665
  }
572
- return { scanned, changed, fixedHash, fixedVersion, fixedTier, addedTimestamps, addedContentType, dryRun, updated: updatedIds };
666
+ const normalizeResp = { scanned, changed, fixedHash, fixedVersion, fixedTier, addedTimestamps, addedContentType, dryRun, updated: updatedIds };
667
+ if (errors.length)
668
+ normalizeResp.errors = errors;
669
+ return normalizeResp;
573
670
  }));
574
671
  // usage_flush (mutation)
575
672
  (0, registry_1.registerHandler)('usage_flush', (0, instructions_shared_1.guard)('usage_flush', () => ({ flushed: true })));
@@ -12,6 +12,8 @@ const features_1 = require("../features");
12
12
  const schemaVersion_1 = require("../../versioning/schemaVersion");
13
13
  const classificationService_1 = require("../classificationService");
14
14
  const ownershipService_1 = require("../ownershipService");
15
+ const instructionRecordValidation_1 = require("../instructionRecordValidation");
16
+ const instructionRecordValidation_2 = require("../instructionRecordValidation");
15
17
  const auditLog_1 = require("../auditLog");
16
18
  const runtimeConfig_1 = require("../../config/runtimeConfig");
17
19
  const manifestManager_1 = require("../manifestManager");
@@ -39,7 +41,7 @@ function parseInlineEntries(rawEntries) {
39
41
  return { error: { error: 'entries JSON parse error', detail: e.message } };
40
42
  }
41
43
  }
42
- (0, registry_1.registerHandler)('index_import', (0, instructions_shared_1.guard)('index_import', (p) => {
44
+ (0, registry_1.registerHandler)('index_import', (0, instructions_shared_1.guard)('index_import', async (p) => {
43
45
  let entries;
44
46
  const mode = p.mode || 'skip';
45
47
  if (Array.isArray(p.entries)) {
@@ -108,10 +110,21 @@ function parseInlineEntries(rawEntries) {
108
110
  let imported = 0, skipped = 0, overwritten = 0;
109
111
  const errors = [];
110
112
  const classifier = new classificationService_1.ClassificationService();
113
+ const skippedIds = new Set();
114
+ const formatImportValidationError = (validationErrors) => `invalid_instruction: ${validationErrors.join('; ')}`;
111
115
  for (const e of entries) {
112
- if (!e || !e.id || !e.title || !e.body) {
113
- const id = e?.id || 'unknown';
114
- errors.push({ id, error: 'missing required fields' });
116
+ const id = e?.id || 'unknown';
117
+ const requiredFieldErrors = [
118
+ !e?.id ? 'id: missing required field' : undefined,
119
+ e?.title === undefined ? 'title: missing required field' : undefined,
120
+ e?.body === undefined ? 'body: missing required field' : undefined,
121
+ e?.priority === undefined ? 'priority: missing required field' : undefined,
122
+ e?.audience === undefined ? 'audience: missing required field' : undefined,
123
+ e?.requirement === undefined ? 'requirement: missing required field' : undefined,
124
+ ].filter((issue) => !!issue);
125
+ const surfaceValidation = e ? (0, instructionRecordValidation_1.validateInstructionInputSurface)(e) : { validationErrors: [], hints: [], schemaRef: 'index_add#input' };
126
+ if (!e || requiredFieldErrors.length || surfaceValidation.validationErrors.length) {
127
+ errors.push({ id, error: formatImportValidationError([...requiredFieldErrors, ...surfaceValidation.validationErrors]) });
115
128
  continue;
116
129
  }
117
130
  const bodyTrimmed = typeof e.body === 'string' ? e.body.trim() : String(e.body);
@@ -121,11 +134,11 @@ function parseInlineEntries(rawEntries) {
121
134
  continue;
122
135
  }
123
136
  const file = path_1.default.join(dir, `${e.id}.json`);
124
- const stImport = (0, indexContext_1.ensureLoaded)();
137
+ const stImport = await (0, indexContext_1.ensureLoadedAsync)();
125
138
  const storeHas = stImport.byId.has(e.id);
126
139
  const fileExists = storeHas || fs_1.default.existsSync(file);
127
140
  const now = new Date().toISOString();
128
- let categories = Array.from(new Set((Array.isArray(e.categories) ? e.categories : []).filter((c) => typeof c === 'string' && c.trim().length > 0).map(c => c.toLowerCase()))).sort();
141
+ let categories = (0, instructions_shared_1.normalizeInputCategories)(e.categories);
129
142
  const primaryCategoryRaw = e.primaryCategory;
130
143
  if (!categories.length) {
131
144
  if (instructionsCfg.requireCategory) {
@@ -163,20 +176,11 @@ function parseInlineEntries(rawEntries) {
163
176
  }
164
177
  if (fileExists && mode === 'skip') {
165
178
  skipped++;
179
+ skippedIds.add(e.id);
166
180
  continue;
167
181
  }
168
- if (fileExists && mode === 'overwrite')
169
- overwritten++;
170
- else if (!fileExists)
171
- imported++;
172
182
  const base = existing ? { ...existing, title: e.title, body: bodyTrimmed, rationale: e.rationale, priority: e.priority, audience: e.audience, requirement: e.requirement, categories, primaryCategory: effectivePrimary, updatedAt: now } : { id: e.id, title: e.title, body: bodyTrimmed, rationale: e.rationale, priority: e.priority, audience: e.audience, requirement: e.requirement, categories, primaryCategory: effectivePrimary, sourceHash: newBodyHash, schemaVersion: schemaVersion_1.SCHEMA_VERSION, deprecatedBy: e.deprecatedBy, createdAt: now, updatedAt: now, riskScore: e.riskScore, createdByAgent: instructionsCfg.agentId, sourceWorkspace: instructionsCfg.workspaceId, extensions: e.extensions };
173
- const govKeys = ['version', 'owner', 'status', 'priorityTier', 'classification', 'lastReviewedAt', 'nextReviewDue', 'changeLog', 'semanticSummary', 'contentType', 'extensions'];
174
- for (const k of govKeys) {
175
- const v = e[k];
176
- if (v !== undefined) {
177
- base[k] = v;
178
- }
179
- }
183
+ (0, instructions_shared_1.applyGovernanceKeys)(base, e, instructions_shared_1.IMPORT_GOVERNANCE_KEYS);
180
184
  if (!base.sourceWorkspace)
181
185
  base.sourceWorkspace = instructionsCfg.workspaceId;
182
186
  base.sourceHash = newBodyHash;
@@ -188,18 +192,47 @@ function parseInlineEntries(rawEntries) {
188
192
  record.updatedAt = new Date().toISOString();
189
193
  }
190
194
  }
195
+ const recordValidation = (0, instructionRecordValidation_1.validateInstructionRecord)(record);
196
+ if (recordValidation.validationErrors.length) {
197
+ errors.push({ id: e.id, error: formatImportValidationError(recordValidation.validationErrors) });
198
+ continue;
199
+ }
191
200
  try {
192
- (0, indexContext_1.writeEntry)(record);
201
+ await (0, indexContext_1.writeEntryAsync)(record);
193
202
  }
194
- catch {
195
- errors.push({ id: e.id, error: 'write-failed' });
203
+ catch (err) {
204
+ if ((0, instructionRecordValidation_2.isInstructionValidationError)(err)) {
205
+ errors.push({ id: e.id, error: formatImportValidationError(err.validationErrors) });
206
+ continue;
207
+ }
208
+ errors.push({ id: e.id, error: `write-failed: ${err.message || 'unknown'}` });
209
+ continue;
196
210
  }
211
+ if (fileExists && mode === 'overwrite')
212
+ overwritten++;
213
+ else if (!fileExists)
214
+ imported++;
197
215
  }
198
216
  (0, indexContext_1.touchIndexVersion)();
199
217
  (0, indexContext_1.invalidate)();
200
- const st = (0, indexContext_1.ensureLoaded)();
201
- const summary = { hash: st.hash, imported, skipped, overwritten, total: entries.length, errors };
202
- (0, auditLog_1.logAudit)('import', entries.map(e => e.id), { imported, skipped, overwritten, errors: errors.length });
218
+ const st = await (0, indexContext_1.ensureLoadedAsync)();
219
+ // Read-back verification: confirm each written entry is visible in the reloaded index
220
+ const verificationErrors = [];
221
+ const writtenIds = entries
222
+ .filter(e => e.id && !errors.some(err => err.id === e.id) && !skippedIds.has(e.id))
223
+ .map(e => e.id);
224
+ for (const id of writtenIds) {
225
+ if (!st.byId.has(id)) {
226
+ verificationErrors.push({ id, error: 'not-in-index-after-reload' });
227
+ }
228
+ }
229
+ if (verificationErrors.length) {
230
+ errors.push(...verificationErrors);
231
+ (0, auditLog_1.logAudit)('import_verification', verificationErrors.map(v => v.id), { missingAfterReload: verificationErrors.length });
232
+ }
233
+ const verifiedCount = writtenIds.length - verificationErrors.length;
234
+ const summary = { hash: st.hash, imported, skipped, overwritten, total: entries.length, errors, verified: verificationErrors.length === 0, verifiedCount, verificationErrorCount: verificationErrors.length };
235
+ (0, auditLog_1.logAudit)('import', entries.map(e => e.id), { imported, skipped, overwritten, errors: errors.length, verified: verificationErrors.length === 0 });
203
236
  (0, manifestManager_1.attemptManifestUpdate)();
204
237
  return summary;
205
238
  }));
@@ -7,40 +7,58 @@ const manifestManager_1 = require("../manifestManager");
7
7
  const features_1 = require("../features");
8
8
  const instructions_shared_1 = require("./instructions.shared");
9
9
  (0, registry_1.registerHandler)('index_governanceHash', () => {
10
+ const reloadFailures = [];
11
+ let reloadSucceeded = false;
12
+ const captureReloadFailure = (stage, error) => {
13
+ const reason = error instanceof Error ? error.message : String(error);
14
+ reloadFailures.push(`${stage}: ${reason}`);
15
+ };
16
+ const reloadState = (stage) => {
17
+ (0, indexContext_1.invalidate)();
18
+ try {
19
+ const loaded = (0, indexContext_1.ensureLoaded)();
20
+ reloadSucceeded = true;
21
+ return loaded;
22
+ }
23
+ catch (error) {
24
+ captureReloadFailure(stage, error);
25
+ throw error;
26
+ }
27
+ };
10
28
  let st = (0, indexContext_1.ensureLoaded)();
11
29
  const now = Date.now();
12
30
  const loadedAgo = now - new Date(st.loadedAt).getTime();
13
31
  if (loadedAgo > 50) {
14
32
  try {
15
33
  // Force reload if stale — trust the store for freshness
16
- (0, indexContext_1.invalidate)();
17
- st = (0, indexContext_1.ensureLoaded)();
34
+ st = reloadState('stale-check');
18
35
  }
19
- catch { /* ignore verification errors */ }
36
+ catch { /* handled below */ }
20
37
  }
21
38
  let projections = st.list.slice().sort((a, b) => a.id.localeCompare(b.id)).map(indexContext_1.projectGovernance);
22
39
  try {
23
40
  const storeCount = st.list.length;
24
41
  if (storeCount && (projections.length === 0 || projections.length < Math.floor(storeCount * 0.9))) {
25
42
  // Late materialization: reload from store to pick up any missing entries
26
- (0, indexContext_1.invalidate)();
27
- st = (0, indexContext_1.ensureLoaded)();
43
+ st = reloadState('late-materialization');
28
44
  projections = st.list.slice().sort((a, b) => a.id.localeCompare(b.id)).map(indexContext_1.projectGovernance);
29
45
  }
30
46
  }
31
- catch { /* ignore defensive reload errors */ }
47
+ catch { /* handled below */ }
32
48
  const governanceHash = (0, indexContext_1.computeGovernanceHash)(st.list);
33
49
  if (projections.length && projections.length < Math.floor(st.list.length * 0.9) || projections.some(p => !p.owner)) {
34
50
  try {
35
- (0, indexContext_1.invalidate)();
36
- const st2 = (0, indexContext_1.ensureLoaded)();
51
+ const st2 = reloadState('projection-repair');
37
52
  projections = st2.list.slice().sort((a, b) => a.id.localeCompare(b.id)).map(indexContext_1.projectGovernance);
38
53
  try {
39
54
  (0, features_1.incrementCounter)('governance:projectionRepair');
40
55
  }
41
- catch { /* ignore */ }
56
+ catch { /* counter-only — non-critical */ }
42
57
  }
43
- catch { /* ignore reload failure */ }
58
+ catch { /* handled below */ }
59
+ }
60
+ if (reloadFailures.length && !reloadSucceeded) {
61
+ throw new Error(`index_governanceHash could not refresh index state: ${reloadFailures.join('; ')}`);
44
62
  }
45
63
  return { count: projections.length, governanceHash, items: projections };
46
64
  });
@@ -48,8 +66,10 @@ const instructions_shared_1 = require("./instructions.shared");
48
66
  const id = p.id;
49
67
  const st = (0, indexContext_1.ensureLoaded)();
50
68
  const existing = st.byId.get(id);
51
- if (!existing)
69
+ if (!existing) {
70
+ (0, auditLog_1.logAudit)('governanceUpdate', id, { changed: false, notFound: true });
52
71
  return { id, notFound: true };
72
+ }
53
73
  // Read from store (in-memory), fall back to disk for JSON backend
54
74
  const record = { ...existing };
55
75
  let changed = false;
@@ -63,6 +83,7 @@ const instructions_shared_1 = require("./instructions.shared");
63
83
  const allowed = ['draft', 'review', 'approved', 'deprecated'];
64
84
  const desired = p.status === 'active' ? 'approved' : p.status;
65
85
  if (!allowed.includes(desired)) {
86
+ (0, auditLog_1.logAudit)('governanceUpdate', id, { changed: false, error: 'invalid status', provided: p.status });
66
87
  return { id, error: 'invalid status', provided: p.status };
67
88
  }
68
89
  if (desired !== record.status) {
@@ -79,26 +100,10 @@ const instructions_shared_1 = require("./instructions.shared");
79
100
  changed = true;
80
101
  }
81
102
  if (bump && bump !== 'none') {
82
- const parts = (record.version || '1.0.0').split('.').map(n => parseInt(n || '0', 10));
83
- while (parts.length < 3)
84
- parts.push(0);
85
- if (bump === 'major')
86
- parts[0]++;
87
- else if (bump === 'minor')
88
- parts[1]++;
89
- else if (bump === 'patch')
90
- parts[2]++;
91
- if (bump === 'major') {
92
- parts[1] = 0;
93
- parts[2] = 0;
94
- }
95
- if (bump === 'minor') {
96
- parts[2] = 0;
97
- }
98
- const newVersion = parts.join('.');
103
+ const newVersion = (0, instructions_shared_1.bumpVersion)(record.version, bump);
99
104
  if (newVersion !== record.version) {
100
105
  record.version = newVersion;
101
- record.changeLog = [...(record.changeLog || []), { version: newVersion, changedAt: now, summary: `manual ${bump} bump via governanceUpdate` }];
106
+ record.changeLog = [...(record.changeLog || []), (0, instructions_shared_1.createChangeLogEntry)(newVersion, `manual ${bump} bump via governanceUpdate`)];
102
107
  changed = true;
103
108
  }
104
109
  }
@@ -108,14 +113,20 @@ const instructions_shared_1 = require("./instructions.shared");
108
113
  try {
109
114
  (0, indexContext_1.writeEntry)(record);
110
115
  }
111
- catch {
112
- return { id, error: 'write-failed' };
116
+ catch (err) {
117
+ const detail = err.message || 'unknown';
118
+ const errorType = err instanceof Error ? err.constructor.name : typeof err;
119
+ const stack = err instanceof Error ? err.stack?.slice(0, 500) : undefined;
120
+ // #132: full detail (including paths) only goes to the audit log; client gets a path-redacted version
121
+ const safeDetail = detail.replace(/[A-Za-z]:\\[^\s'"`]+/g, '<redacted-path>').replace(/\/(?:[^\s/'"`]+\/)+[^\s/'"`]+/g, '<redacted-path>');
122
+ (0, auditLog_1.logAudit)('governanceUpdate', id, { changed: false, error: detail, errorType, stack, writeFailure: true });
123
+ return { id, error: 'write-failed', detail: safeDetail, errorType };
113
124
  }
114
125
  (0, indexContext_1.touchIndexVersion)();
115
126
  (0, indexContext_1.invalidate)();
116
127
  (0, indexContext_1.ensureLoaded)();
117
128
  const resp = { id, changed: true, version: record.version, owner: record.owner, status: record.status, lastReviewedAt: record.lastReviewedAt, nextReviewDue: record.nextReviewDue };
118
- (0, auditLog_1.logAudit)('governanceUpdate', id, { changed: true, version: record.version });
129
+ (0, auditLog_1.logAudit)('governanceUpdate', id, { changed: true, version: record.version, owner: record.owner, status: record.status, lastReviewedAt: record.lastReviewedAt, nextReviewDue: record.nextReviewDue });
119
130
  (0, manifestManager_1.attemptManifestUpdate)();
120
131
  return resp;
121
132
  }));