@jagilber-org/index-server 1.19.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 (360) hide show
  1. package/CHANGELOG.md +1218 -0
  2. package/CODE_OF_CONDUCT.md +49 -0
  3. package/CONTRIBUTING.md +75 -0
  4. package/LICENSE +21 -0
  5. package/README.md +523 -0
  6. package/SECURITY.md +50 -0
  7. package/dist/config/configUtils.d.ts +11 -0
  8. package/dist/config/configUtils.js +87 -0
  9. package/dist/config/dashboardConfig.d.ts +45 -0
  10. package/dist/config/dashboardConfig.js +63 -0
  11. package/dist/config/defaultValues.d.ts +61 -0
  12. package/dist/config/defaultValues.js +70 -0
  13. package/dist/config/dirConstants.d.ts +17 -0
  14. package/dist/config/dirConstants.js +28 -0
  15. package/dist/config/featureConfig.d.ts +61 -0
  16. package/dist/config/featureConfig.js +121 -0
  17. package/dist/config/runtimeConfig.d.ts +145 -0
  18. package/dist/config/runtimeConfig.js +334 -0
  19. package/dist/config/serverConfig.d.ts +90 -0
  20. package/dist/config/serverConfig.js +164 -0
  21. package/dist/dashboard/analytics/AnalyticsEngine.d.ts +142 -0
  22. package/dist/dashboard/analytics/AnalyticsEngine.js +373 -0
  23. package/dist/dashboard/analytics/BusinessIntelligence.d.ts +187 -0
  24. package/dist/dashboard/analytics/BusinessIntelligence.js +594 -0
  25. package/dist/dashboard/client/admin.html +2150 -0
  26. package/dist/dashboard/client/chunks/mermaid-layout-elk.esm.min/chunk-SP2CHFBE.mjs +1 -0
  27. package/dist/dashboard/client/chunks/mermaid-layout-elk.esm.min/render-T6MDALS3.mjs +27 -0
  28. package/dist/dashboard/client/css/admin.css +1466 -0
  29. package/dist/dashboard/client/js/admin.boot.js +359 -0
  30. package/dist/dashboard/client/js/admin.config.js +196 -0
  31. package/dist/dashboard/client/js/admin.embeddings.js +425 -0
  32. package/dist/dashboard/client/js/admin.graph.js +583 -0
  33. package/dist/dashboard/client/js/admin.instances.js +120 -0
  34. package/dist/dashboard/client/js/admin.instructions.js +552 -0
  35. package/dist/dashboard/client/js/admin.logs.js +113 -0
  36. package/dist/dashboard/client/js/admin.maintenance.js +354 -0
  37. package/dist/dashboard/client/js/admin.messaging.js +635 -0
  38. package/dist/dashboard/client/js/admin.monitor.js +181 -0
  39. package/dist/dashboard/client/js/admin.overview.js +221 -0
  40. package/dist/dashboard/client/js/admin.performance.js +61 -0
  41. package/dist/dashboard/client/js/admin.sessions.js +293 -0
  42. package/dist/dashboard/client/js/admin.sqlite.js +366 -0
  43. package/dist/dashboard/client/js/admin.utils.js +49 -0
  44. package/dist/dashboard/client/js/chart.umd.js +14 -0
  45. package/dist/dashboard/client/js/elk.bundled.js +6696 -0
  46. package/dist/dashboard/client/js/marked.umd.js +74 -0
  47. package/dist/dashboard/client/js/mermaid.min.js +3022 -0
  48. package/dist/dashboard/client/mermaid-layout-elk.esm.min.mjs +1 -0
  49. package/dist/dashboard/export/DataExporter.d.ts +169 -0
  50. package/dist/dashboard/export/DataExporter.js +737 -0
  51. package/dist/dashboard/export/exporters/csvExporter.d.ts +11 -0
  52. package/dist/dashboard/export/exporters/csvExporter.js +46 -0
  53. package/dist/dashboard/export/exporters/exportTypes.d.ts +89 -0
  54. package/dist/dashboard/export/exporters/exportTypes.js +5 -0
  55. package/dist/dashboard/export/exporters/jsonExporter.d.ts +7 -0
  56. package/dist/dashboard/export/exporters/jsonExporter.js +22 -0
  57. package/dist/dashboard/export/exporters/xmlExporter.d.ts +17 -0
  58. package/dist/dashboard/export/exporters/xmlExporter.js +175 -0
  59. package/dist/dashboard/integration/APIIntegration.d.ts +41 -0
  60. package/dist/dashboard/integration/APIIntegration.js +95 -0
  61. package/dist/dashboard/security/SecurityMonitor.d.ts +167 -0
  62. package/dist/dashboard/security/SecurityMonitor.js +559 -0
  63. package/dist/dashboard/server/AdminPanel.d.ts +183 -0
  64. package/dist/dashboard/server/AdminPanel.js +792 -0
  65. package/dist/dashboard/server/AdminPanelConfig.d.ts +42 -0
  66. package/dist/dashboard/server/AdminPanelConfig.js +80 -0
  67. package/dist/dashboard/server/AdminPanelState.d.ts +47 -0
  68. package/dist/dashboard/server/AdminPanelState.js +214 -0
  69. package/dist/dashboard/server/ApiRoutes.d.ts +17 -0
  70. package/dist/dashboard/server/ApiRoutes.js +149 -0
  71. package/dist/dashboard/server/DashboardServer.d.ts +49 -0
  72. package/dist/dashboard/server/DashboardServer.js +159 -0
  73. package/dist/dashboard/server/FileMetricsStorage.d.ts +49 -0
  74. package/dist/dashboard/server/FileMetricsStorage.js +195 -0
  75. package/dist/dashboard/server/HttpTransport.d.ts +23 -0
  76. package/dist/dashboard/server/HttpTransport.js +116 -0
  77. package/dist/dashboard/server/InstanceManager.d.ts +53 -0
  78. package/dist/dashboard/server/InstanceManager.js +284 -0
  79. package/dist/dashboard/server/KnowledgeStore.d.ts +35 -0
  80. package/dist/dashboard/server/KnowledgeStore.js +105 -0
  81. package/dist/dashboard/server/LeaderElection.d.ts +81 -0
  82. package/dist/dashboard/server/LeaderElection.js +268 -0
  83. package/dist/dashboard/server/MetricsCollector.d.ts +200 -0
  84. package/dist/dashboard/server/MetricsCollector.js +803 -0
  85. package/dist/dashboard/server/SessionPersistenceManager.d.ts +88 -0
  86. package/dist/dashboard/server/SessionPersistenceManager.js +457 -0
  87. package/dist/dashboard/server/ThinClient.d.ts +64 -0
  88. package/dist/dashboard/server/ThinClient.js +237 -0
  89. package/dist/dashboard/server/WebSocketManager.d.ts +161 -0
  90. package/dist/dashboard/server/WebSocketManager.js +463 -0
  91. package/dist/dashboard/server/httpLifecycle.d.ts +17 -0
  92. package/dist/dashboard/server/httpLifecycle.js +35 -0
  93. package/dist/dashboard/server/legacyDashboardHtml.d.ts +9 -0
  94. package/dist/dashboard/server/legacyDashboardHtml.js +618 -0
  95. package/dist/dashboard/server/legacyDashboardStyles.d.ts +5 -0
  96. package/dist/dashboard/server/legacyDashboardStyles.js +490 -0
  97. package/dist/dashboard/server/metricsAggregation.d.ts +252 -0
  98. package/dist/dashboard/server/metricsAggregation.js +206 -0
  99. package/dist/dashboard/server/metricsSerializer.d.ts +25 -0
  100. package/dist/dashboard/server/metricsSerializer.js +195 -0
  101. package/dist/dashboard/server/routes/admin.routes.d.ts +16 -0
  102. package/dist/dashboard/server/routes/admin.routes.js +596 -0
  103. package/dist/dashboard/server/routes/alerts.routes.d.ts +7 -0
  104. package/dist/dashboard/server/routes/alerts.routes.js +93 -0
  105. package/dist/dashboard/server/routes/api.feedback.routes.d.ts +73 -0
  106. package/dist/dashboard/server/routes/api.feedback.routes.js +171 -0
  107. package/dist/dashboard/server/routes/api.instructions.routes.d.ts +101 -0
  108. package/dist/dashboard/server/routes/api.instructions.routes.js +213 -0
  109. package/dist/dashboard/server/routes/api.usage.routes.d.ts +57 -0
  110. package/dist/dashboard/server/routes/api.usage.routes.js +374 -0
  111. package/dist/dashboard/server/routes/embeddings.routes.d.ts +6 -0
  112. package/dist/dashboard/server/routes/embeddings.routes.js +246 -0
  113. package/dist/dashboard/server/routes/graph.routes.d.ts +6 -0
  114. package/dist/dashboard/server/routes/graph.routes.js +280 -0
  115. package/dist/dashboard/server/routes/index.d.ts +38 -0
  116. package/dist/dashboard/server/routes/index.js +194 -0
  117. package/dist/dashboard/server/routes/instances.routes.d.ts +6 -0
  118. package/dist/dashboard/server/routes/instances.routes.js +35 -0
  119. package/dist/dashboard/server/routes/instructions.routes.d.ts +8 -0
  120. package/dist/dashboard/server/routes/instructions.routes.js +336 -0
  121. package/dist/dashboard/server/routes/knowledge.routes.d.ts +6 -0
  122. package/dist/dashboard/server/routes/knowledge.routes.js +82 -0
  123. package/dist/dashboard/server/routes/logs.routes.d.ts +6 -0
  124. package/dist/dashboard/server/routes/logs.routes.js +164 -0
  125. package/dist/dashboard/server/routes/messaging.routes.d.ts +16 -0
  126. package/dist/dashboard/server/routes/messaging.routes.js +293 -0
  127. package/dist/dashboard/server/routes/metrics.routes.d.ts +10 -0
  128. package/dist/dashboard/server/routes/metrics.routes.js +346 -0
  129. package/dist/dashboard/server/routes/scripts.routes.d.ts +9 -0
  130. package/dist/dashboard/server/routes/scripts.routes.js +84 -0
  131. package/dist/dashboard/server/routes/sqlite.routes.d.ts +9 -0
  132. package/dist/dashboard/server/routes/sqlite.routes.js +569 -0
  133. package/dist/dashboard/server/routes/status.routes.d.ts +7 -0
  134. package/dist/dashboard/server/routes/status.routes.js +183 -0
  135. package/dist/dashboard/server/routes/synthetic.routes.d.ts +7 -0
  136. package/dist/dashboard/server/routes/synthetic.routes.js +195 -0
  137. package/dist/dashboard/server/routes/tools.routes.d.ts +6 -0
  138. package/dist/dashboard/server/routes/tools.routes.js +46 -0
  139. package/dist/dashboard/server/routes/usage.routes.d.ts +6 -0
  140. package/dist/dashboard/server/routes/usage.routes.js +25 -0
  141. package/dist/dashboard/server/wsInit.d.ts +16 -0
  142. package/dist/dashboard/server/wsInit.js +35 -0
  143. package/dist/externalClientLib.d.ts +1 -0
  144. package/dist/externalClientLib.js +2 -0
  145. package/dist/minimal/index.d.ts +1 -0
  146. package/dist/minimal/index.js +140 -0
  147. package/dist/models/SessionPersistence.d.ts +115 -0
  148. package/dist/models/SessionPersistence.js +66 -0
  149. package/dist/models/instruction.d.ts +45 -0
  150. package/dist/models/instruction.js +2 -0
  151. package/dist/perf/benchmark.d.ts +1 -0
  152. package/dist/perf/benchmark.js +50 -0
  153. package/dist/portableClientWrapper.d.ts +1 -0
  154. package/dist/portableClientWrapper.js +2 -0
  155. package/dist/schemas/index.d.ts +128 -0
  156. package/dist/schemas/index.js +371 -0
  157. package/dist/scripts/runPerformanceBaseline.d.ts +1 -0
  158. package/dist/scripts/runPerformanceBaseline.js +17 -0
  159. package/dist/server/handshakeManager.d.ts +25 -0
  160. package/dist/server/handshakeManager.js +472 -0
  161. package/dist/server/index-server.d.ts +56 -0
  162. package/dist/server/index-server.js +822 -0
  163. package/dist/server/registry.d.ts +44 -0
  164. package/dist/server/registry.js +236 -0
  165. package/dist/server/sdkServer.d.ts +8 -0
  166. package/dist/server/sdkServer.js +299 -0
  167. package/dist/server/shutdownGuard.d.ts +41 -0
  168. package/dist/server/shutdownGuard.js +52 -0
  169. package/dist/server/thin-client.d.ts +22 -0
  170. package/dist/server/thin-client.js +111 -0
  171. package/dist/server/transport.d.ts +41 -0
  172. package/dist/server/transport.js +312 -0
  173. package/dist/server/transportFactory.d.ts +21 -0
  174. package/dist/server/transportFactory.js +429 -0
  175. package/dist/services/atomicFs.d.ts +22 -0
  176. package/dist/services/atomicFs.js +103 -0
  177. package/dist/services/auditLog.d.ts +38 -0
  178. package/dist/services/auditLog.js +142 -0
  179. package/dist/services/autoBackup.d.ts +14 -0
  180. package/dist/services/autoBackup.js +171 -0
  181. package/dist/services/autoSplit.d.ts +32 -0
  182. package/dist/services/autoSplit.js +113 -0
  183. package/dist/services/backupZip.d.ts +25 -0
  184. package/dist/services/backupZip.js +110 -0
  185. package/dist/services/bootstrapGating.d.ts +123 -0
  186. package/dist/services/bootstrapGating.js +221 -0
  187. package/dist/services/canonical.d.ts +23 -0
  188. package/dist/services/canonical.js +65 -0
  189. package/dist/services/categoryRules.d.ts +7 -0
  190. package/dist/services/categoryRules.js +37 -0
  191. package/dist/services/classificationService.d.ts +42 -0
  192. package/dist/services/classificationService.js +168 -0
  193. package/dist/services/embeddingService.d.ts +62 -0
  194. package/dist/services/embeddingService.js +259 -0
  195. package/dist/services/errors.d.ts +22 -0
  196. package/dist/services/errors.js +31 -0
  197. package/dist/services/featureFlags.d.ts +25 -0
  198. package/dist/services/featureFlags.js +89 -0
  199. package/dist/services/features.d.ts +13 -0
  200. package/dist/services/features.js +35 -0
  201. package/dist/services/handlers/instructions.add.d.ts +1 -0
  202. package/dist/services/handlers/instructions.add.js +496 -0
  203. package/dist/services/handlers/instructions.groom.d.ts +1 -0
  204. package/dist/services/handlers/instructions.groom.js +523 -0
  205. package/dist/services/handlers/instructions.import.d.ts +1 -0
  206. package/dist/services/handlers/instructions.import.js +173 -0
  207. package/dist/services/handlers/instructions.patch.d.ts +1 -0
  208. package/dist/services/handlers/instructions.patch.js +167 -0
  209. package/dist/services/handlers/instructions.query.d.ts +163 -0
  210. package/dist/services/handlers/instructions.query.js +522 -0
  211. package/dist/services/handlers/instructions.reload.d.ts +1 -0
  212. package/dist/services/handlers/instructions.reload.js +13 -0
  213. package/dist/services/handlers/instructions.remove.d.ts +1 -0
  214. package/dist/services/handlers/instructions.remove.js +118 -0
  215. package/dist/services/handlers/instructions.shared.d.ts +31 -0
  216. package/dist/services/handlers/instructions.shared.js +124 -0
  217. package/dist/services/handlers.activation.d.ts +1 -0
  218. package/dist/services/handlers.activation.js +203 -0
  219. package/dist/services/handlers.bootstrap.d.ts +1 -0
  220. package/dist/services/handlers.bootstrap.js +38 -0
  221. package/dist/services/handlers.dashboardConfig.d.ts +34 -0
  222. package/dist/services/handlers.dashboardConfig.js +108 -0
  223. package/dist/services/handlers.diagnostics.d.ts +1 -0
  224. package/dist/services/handlers.diagnostics.js +64 -0
  225. package/dist/services/handlers.feedback.d.ts +15 -0
  226. package/dist/services/handlers.feedback.js +378 -0
  227. package/dist/services/handlers.gates.d.ts +1 -0
  228. package/dist/services/handlers.gates.js +46 -0
  229. package/dist/services/handlers.graph.d.ts +53 -0
  230. package/dist/services/handlers.graph.js +231 -0
  231. package/dist/services/handlers.help.d.ts +1 -0
  232. package/dist/services/handlers.help.js +119 -0
  233. package/dist/services/handlers.instructionSchema.d.ts +1 -0
  234. package/dist/services/handlers.instructionSchema.js +227 -0
  235. package/dist/services/handlers.instructions.d.ts +8 -0
  236. package/dist/services/handlers.instructions.js +14 -0
  237. package/dist/services/handlers.instructionsDiagnostics.d.ts +1 -0
  238. package/dist/services/handlers.instructionsDiagnostics.js +14 -0
  239. package/dist/services/handlers.integrity.d.ts +1 -0
  240. package/dist/services/handlers.integrity.js +35 -0
  241. package/dist/services/handlers.manifest.d.ts +1 -0
  242. package/dist/services/handlers.manifest.js +24 -0
  243. package/dist/services/handlers.messaging.d.ts +12 -0
  244. package/dist/services/handlers.messaging.js +203 -0
  245. package/dist/services/handlers.metrics.d.ts +1 -0
  246. package/dist/services/handlers.metrics.js +43 -0
  247. package/dist/services/handlers.promote.d.ts +1 -0
  248. package/dist/services/handlers.promote.js +306 -0
  249. package/dist/services/handlers.prompt.d.ts +1 -0
  250. package/dist/services/handlers.prompt.js +7 -0
  251. package/dist/services/handlers.search.d.ts +69 -0
  252. package/dist/services/handlers.search.js +645 -0
  253. package/dist/services/handlers.testPrimitive.d.ts +1 -0
  254. package/dist/services/handlers.testPrimitive.js +5 -0
  255. package/dist/services/handlers.trace.d.ts +1 -0
  256. package/dist/services/handlers.trace.js +31 -0
  257. package/dist/services/handlers.usage.d.ts +1 -0
  258. package/dist/services/handlers.usage.js +11 -0
  259. package/dist/services/hotScore.d.ts +137 -0
  260. package/dist/services/hotScore.js +244 -0
  261. package/dist/services/indexContext.d.ts +117 -0
  262. package/dist/services/indexContext.js +968 -0
  263. package/dist/services/indexLoader.d.ts +44 -0
  264. package/dist/services/indexLoader.js +921 -0
  265. package/dist/services/indexRepository.d.ts +32 -0
  266. package/dist/services/indexRepository.js +71 -0
  267. package/dist/services/indexingService.d.ts +1 -0
  268. package/dist/services/indexingService.js +2 -0
  269. package/dist/services/instructions.dispatcher.d.ts +1 -0
  270. package/dist/services/instructions.dispatcher.js +231 -0
  271. package/dist/services/logPrefix.d.ts +1 -0
  272. package/dist/services/logPrefix.js +30 -0
  273. package/dist/services/logger.d.ts +52 -0
  274. package/dist/services/logger.js +268 -0
  275. package/dist/services/manifestManager.d.ts +82 -0
  276. package/dist/services/manifestManager.js +200 -0
  277. package/dist/services/messaging/agentMailbox.d.ts +60 -0
  278. package/dist/services/messaging/agentMailbox.js +353 -0
  279. package/dist/services/messaging/messagingPersistence.d.ts +20 -0
  280. package/dist/services/messaging/messagingPersistence.js +111 -0
  281. package/dist/services/messaging/messagingTypes.d.ts +150 -0
  282. package/dist/services/messaging/messagingTypes.js +66 -0
  283. package/dist/services/ownershipService.d.ts +1 -0
  284. package/dist/services/ownershipService.js +38 -0
  285. package/dist/services/performanceBaseline.d.ts +19 -0
  286. package/dist/services/performanceBaseline.js +210 -0
  287. package/dist/services/preflight.d.ts +12 -0
  288. package/dist/services/preflight.js +79 -0
  289. package/dist/services/promptReviewService.d.ts +44 -0
  290. package/dist/services/promptReviewService.js +101 -0
  291. package/dist/services/responseEnvelope.d.ts +6 -0
  292. package/dist/services/responseEnvelope.js +25 -0
  293. package/dist/services/seedBootstrap.d.ts +34 -0
  294. package/dist/services/seedBootstrap.js +427 -0
  295. package/dist/services/storage/factory.d.ts +17 -0
  296. package/dist/services/storage/factory.js +35 -0
  297. package/dist/services/storage/hashUtils.d.ts +11 -0
  298. package/dist/services/storage/hashUtils.js +35 -0
  299. package/dist/services/storage/index.d.ts +12 -0
  300. package/dist/services/storage/index.js +18 -0
  301. package/dist/services/storage/jsonFileStore.d.ts +32 -0
  302. package/dist/services/storage/jsonFileStore.js +241 -0
  303. package/dist/services/storage/migrationEngine.d.ts +35 -0
  304. package/dist/services/storage/migrationEngine.js +93 -0
  305. package/dist/services/storage/sqliteMessageStore.d.ts +53 -0
  306. package/dist/services/storage/sqliteMessageStore.js +146 -0
  307. package/dist/services/storage/sqliteSchema.d.ts +12 -0
  308. package/dist/services/storage/sqliteSchema.js +122 -0
  309. package/dist/services/storage/sqliteStore.d.ts +41 -0
  310. package/dist/services/storage/sqliteStore.js +339 -0
  311. package/dist/services/storage/sqliteUsageStore.d.ts +35 -0
  312. package/dist/services/storage/sqliteUsageStore.js +94 -0
  313. package/dist/services/storage/types.d.ts +171 -0
  314. package/dist/services/storage/types.js +12 -0
  315. package/dist/services/toolHandlers.d.ts +23 -0
  316. package/dist/services/toolHandlers.js +50 -0
  317. package/dist/services/toolRegistry.d.ts +20 -0
  318. package/dist/services/toolRegistry.js +490 -0
  319. package/dist/services/toolRegistry.zod.d.ts +10 -0
  320. package/dist/services/toolRegistry.zod.js +323 -0
  321. package/dist/services/tracing.d.ts +26 -0
  322. package/dist/services/tracing.js +260 -0
  323. package/dist/services/usageBuckets.d.ts +161 -0
  324. package/dist/services/usageBuckets.js +364 -0
  325. package/dist/services/validationService.d.ts +38 -0
  326. package/dist/services/validationService.js +125 -0
  327. package/dist/utils/BufferRing.d.ts +203 -0
  328. package/dist/utils/BufferRing.js +551 -0
  329. package/dist/utils/BufferRingExamples.d.ts +55 -0
  330. package/dist/utils/BufferRingExamples.js +188 -0
  331. package/dist/utils/envUtils.d.ts +42 -0
  332. package/dist/utils/envUtils.js +80 -0
  333. package/dist/utils/memoryMonitor.d.ts +83 -0
  334. package/dist/utils/memoryMonitor.js +275 -0
  335. package/dist/versioning/schemaVersion.d.ts +6 -0
  336. package/dist/versioning/schemaVersion.js +93 -0
  337. package/package.json +134 -0
  338. package/schemas/README.md +13 -0
  339. package/schemas/feedback-entry.schema.json +27 -0
  340. package/schemas/graph-export-v2.schema.json +60 -0
  341. package/schemas/index-server.code-schema.json +38477 -0
  342. package/schemas/instruction.schema.json +262 -0
  343. package/schemas/json-schema/SessionPersistence-persisted-admin-session.schema.json +54 -0
  344. package/schemas/json-schema/SessionPersistence-persisted-session-history-entry.schema.json +51 -0
  345. package/schemas/json-schema/SessionPersistence-persisted-web-socket-connection.schema.json +54 -0
  346. package/schemas/json-schema/SessionPersistence-session-persistence-config.schema.json +110 -0
  347. package/schemas/json-schema/SessionPersistence-session-persistence-data.schema.json +229 -0
  348. package/schemas/json-schema/SessionPersistence-session-persistence-manifest.schema.json +109 -0
  349. package/schemas/json-schema/SessionPersistence-session-persistence-metadata.schema.json +55 -0
  350. package/schemas/json-schema/instruction-audience-scope.schema.json +14 -0
  351. package/schemas/json-schema/instruction-content-type.schema.json +17 -0
  352. package/schemas/json-schema/instruction-instruction-entry.schema.json +206 -0
  353. package/schemas/json-schema/instruction-requirement-level.schema.json +16 -0
  354. package/schemas/manifest.json +78 -0
  355. package/schemas/manifest.schema.json +33 -0
  356. package/schemas/usage-batch.schema.json +16 -0
  357. package/schemas/usage-buckets.schema.json +30 -0
  358. package/schemas/usage-event.schema.json +17 -0
  359. package/scripts/copy-dashboard-assets.mjs +170 -0
  360. package/scripts/setup-hooks.cjs +28 -0
@@ -0,0 +1,921 @@
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.IndexLoader = void 0;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const crypto_1 = __importDefault(require("crypto"));
10
+ const classificationService_1 = require("./classificationService");
11
+ const schemaVersion_1 = require("../versioning/schemaVersion");
12
+ const ajv_1 = __importDefault(require("ajv"));
13
+ // Ajv v8 needs explicit format support when strict mode or newer setups; add common formats
14
+ const ajv_formats_1 = __importDefault(require("ajv-formats"));
15
+ // Ensure https draft-07 meta-schema is recognized when $schema uses TLS URL
16
+ const json_schema_draft_07_json_1 = __importDefault(require("ajv/dist/refs/json-schema-draft-07.json"));
17
+ const instruction_schema_json_1 = __importDefault(require("../../schemas/instruction.schema.json"));
18
+ // Normal-verbosity tracing (level 1+) for per-file load lifecycle
19
+ const tracing_1 = require("./tracing");
20
+ const runtimeConfig_1 = require("../config/runtimeConfig");
21
+ const autoSplit_1 = require("./autoSplit");
22
+ class IndexLoader {
23
+ baseDir;
24
+ classifier;
25
+ constructor(baseDir = (0, runtimeConfig_1.getRuntimeConfig)().index.baseDir, classifier = new classificationService_1.ClassificationService()) {
26
+ this.baseDir = baseDir;
27
+ this.classifier = classifier;
28
+ }
29
+ /**
30
+ * Robust JSON file reader with retry/backoff for transient Windows / network FS issues (EPERM/EBUSY/EACCES)
31
+ * and partial write races (empty file or truncated JSON). Ensures we rarely skip valid instructions due
32
+ * to momentary locks while another process is atomically renaming/writing.
33
+ */
34
+ readJsonWithRetry(file) {
35
+ const { attempts, backoffMs } = (0, runtimeConfig_1.getRuntimeConfig)().index.readRetries;
36
+ const maxAttempts = Math.max(1, attempts);
37
+ const baseBackoff = Math.max(1, backoffMs);
38
+ let lastErr = null;
39
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
40
+ try {
41
+ let raw = fs_1.default.readFileSync(file, 'utf8');
42
+ // Strip UTF-8 BOM (U+FEFF) — PowerShell and some editors write BOM by default which breaks JSON.parse
43
+ if (raw.charCodeAt(0) === 0xFEFF)
44
+ raw = raw.slice(1);
45
+ // Treat empty content as transient (likely race) unless final attempt
46
+ if (!raw.trim()) {
47
+ if (attempt === maxAttempts)
48
+ return {};
49
+ throw new Error('empty file transient');
50
+ }
51
+ return JSON.parse(raw);
52
+ }
53
+ catch (err) {
54
+ const code = err.code;
55
+ const transient = code === 'EPERM' || code === 'EBUSY' || code === 'EACCES' || code === 'ENOENT' || (err instanceof Error && /transient|JSON/.test(err.message));
56
+ if (!transient || attempt === maxAttempts) {
57
+ lastErr = err;
58
+ break;
59
+ }
60
+ 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
+ }
65
+ }
66
+ if (lastErr)
67
+ throw lastErr instanceof Error ? lastErr : new Error('readJsonWithRetry failed');
68
+ return {}; // unreachable but satisfies typing
69
+ }
70
+ load() {
71
+ const runtimeConfig = (0, runtimeConfig_1.getRuntimeConfig)();
72
+ const IndexConfig = runtimeConfig.index;
73
+ const dir = path_1.default.resolve(this.baseDir);
74
+ const loadStart = Date.now();
75
+ if ((0, tracing_1.traceEnabled)(1)) {
76
+ try {
77
+ (0, tracing_1.emitTrace)('[trace:index:load-begin]', { dir });
78
+ }
79
+ catch { /* ignore */ }
80
+ }
81
+ if (!fs_1.default.existsSync(dir))
82
+ return { entries: [], errors: [{ file: dir, error: 'missing directory' }], hash: '' };
83
+ // Normalization audit logging (optional): if INDEX_SERVER_NORMALIZATION_LOG is set, we
84
+ // capture per-file normalization deltas (only when a rewrite-worthy change occurs) and
85
+ // emit them as JSONL at the end of the load cycle. This creates a lightweight, append-
86
+ // only forensic trail for production migrations without impacting the hot path when
87
+ // disabled.
88
+ const normalizationSetting = IndexConfig.normalizationLog;
89
+ const normLogTarget = normalizationSetting === true
90
+ ? path_1.default.join(process.cwd(), 'logs', 'index-normalization.log')
91
+ : typeof normalizationSetting === 'string'
92
+ ? normalizationSetting
93
+ : undefined;
94
+ const normLogRecords = [];
95
+ // Lightweight in-process memoization to reduce repeated parse/validate cost when ALWAYS_RELOAD is set.
96
+ // Enabled when index memoization is active, or when reloadAlways is true without an explicit memoize disable flag.
97
+ // Semantics preserved: directory is still scanned each load; changed files (mtime/size) are re-read.
98
+ // Cache key: absolute file path; validation skipped only if size + mtime unchanged.
99
+ // NOTE: This deliberately trusts OS mtime granularity (sufficient for test/runtime reload cadence).
100
+ const memoryCacheEnabled = IndexConfig.memoize || (IndexConfig.reloadAlways && !IndexConfig.memoizeDisabledExplicitly);
101
+ const hashMemoEnabled = IndexConfig.memoizeHash;
102
+ const buildSig = `schema:${schemaVersion_1.SCHEMA_VERSION}`; // extend with classifier / normalization version if those become versioned
103
+ // Module-level singleton map (attached to globalThis to survive module reloads in test environments without duplicating state)
104
+ const globalAny = globalThis;
105
+ if (!globalAny.__MCP_INDEX_SERVER_MEMO) {
106
+ globalAny.__MCP_INDEX_SERVER_MEMO = new Map();
107
+ }
108
+ const indexMemo = globalAny.__MCP_INDEX_SERVER_MEMO;
109
+ let cacheHits = 0;
110
+ let hashHits = 0;
111
+ const ajv = new ajv_1.default({ allErrors: true, strict: false });
112
+ // Add standard date-time, uri, etc. formats
113
+ (0, ajv_formats_1.default)(ajv);
114
+ // Register draft-07 meta schema under https id (with and without trailing #) if not present.
115
+ // Some schema files include $schema value with trailing '#', so we defensively register both forms.
116
+ try {
117
+ const httpsIdNoHash = 'https://json-schema.org/draft-07/schema';
118
+ const httpsIdHash = 'https://json-schema.org/draft-07/schema#';
119
+ if (!ajv.getSchema(httpsIdNoHash)) {
120
+ ajv.addMetaSchema({ ...json_schema_draft_07_json_1.default, $id: httpsIdNoHash });
121
+ }
122
+ if (!ajv.getSchema(httpsIdHash)) {
123
+ // Provide alias with trailing # (clone to avoid mutating previously added object)
124
+ ajv.addMetaSchema({ ...json_schema_draft_07_json_1.default, $id: httpsIdHash });
125
+ }
126
+ }
127
+ catch { /* ignore meta-schema registration issues */ }
128
+ // Patch schema body maxLength from config before compiling (allows INDEX_SERVER_BODY_MAX_LENGTH override)
129
+ const bodyMaxLen = IndexConfig.bodyMaxLength || 20000;
130
+ const autoSplit = IndexConfig.autoSplitOversized === true;
131
+ const schemaCopy = JSON.parse(JSON.stringify(instruction_schema_json_1.default));
132
+ try {
133
+ const props = schemaCopy.properties;
134
+ if (props?.body)
135
+ props.body.maxLength = bodyMaxLen;
136
+ }
137
+ catch { /* ignore schema patch failure */ }
138
+ const validate = ajv.compile(schemaCopy);
139
+ let files = fs_1.default.readdirSync(dir).filter(f => f.endsWith('.json'));
140
+ // Exclude internal manifest file if present
141
+ const MANIFEST_NAME = '_manifest.json';
142
+ files = files.filter(f => f !== MANIFEST_NAME);
143
+ // Optional index size limit for performance (INDEX_SERVER_MAX_FILES)
144
+ const maxFiles = IndexConfig.maxFiles;
145
+ if (maxFiles && maxFiles > 0 && files.length > maxFiles) {
146
+ const line = JSON.stringify({
147
+ level: 'warn',
148
+ event: 'index-size-limit-exceeded',
149
+ found: files.length,
150
+ limit: maxFiles,
151
+ message: `Index contains ${files.length} files but limit is ${maxFiles}. Taking first ${maxFiles} files. Set INDEX_SERVER_MAX_FILES higher or remove limit.`
152
+ });
153
+ process.stderr.write(line + '\n');
154
+ files = files.slice(0, maxFiles);
155
+ }
156
+ const entries = [];
157
+ const errors = [];
158
+ // File-level trace (opt-in) surfaces every scanned file decision so higher-level diagnostics
159
+ // can correlate missingOnIndex IDs with explicit acceptance / rejection reasons. Enable by
160
+ // setting INDEX_SERVER_FILE_TRACE=1 together with INDEX_SERVER_TRACE_ALL for broader context.
161
+ const fileTraceEnabled = IndexConfig.fileTrace;
162
+ const trace = fileTraceEnabled ? [] : undefined;
163
+ // New lightweight always-available (level>=1) sequential tracing with cumulative counters.
164
+ let scannedSoFar = 0;
165
+ let acceptedSoFar = 0;
166
+ // Reason counters (accepted tracked separately). We increment a reason only on *skipped* decisions.
167
+ const reasonCounts = {};
168
+ const salvageCounts = {};
169
+ const softWarnings = {};
170
+ const bump = (r) => { reasonCounts[r] = (reasonCounts[r] || 0) + 1; };
171
+ const salvage = (k) => { salvageCounts[k] = (salvageCounts[k] || 0) + 1; };
172
+ const warnSoft = (k) => { softWarnings[k] = (softWarnings[k] || 0) + 1; };
173
+ // Helper for unconditional lightweight event emission (stdout/stderr JSON line) independent of trace flags.
174
+ // Emission gating: in high-volume scenarios (CI coverage runs), per-file events can create
175
+ // extremely large logs that exceed fetch limits. Set INDEX_SERVER_EVENT_SILENT=1 to suppress
176
+ // individual file events while preserving trace (when enabled) and the final summary.
177
+ const suppressIndexEvents = IndexConfig.eventSilent;
178
+ const emitIndexEvent = (ev) => {
179
+ if (suppressIndexEvents)
180
+ return; // fast path
181
+ try {
182
+ const line = JSON.stringify({ level: 'info', event: 'index-file', ...ev });
183
+ process.stderr.write(line + '\n');
184
+ }
185
+ catch { /* ignore */ }
186
+ };
187
+ for (const f of files) {
188
+ scannedSoFar++;
189
+ if ((0, tracing_1.traceEnabled)(1)) {
190
+ try {
191
+ (0, tracing_1.emitTrace)('[trace:index:file-begin]', { file: f, index: scannedSoFar - 1, total: files.length });
192
+ }
193
+ catch { /* ignore */ }
194
+ }
195
+ else {
196
+ // Always-on minimal begin event
197
+ emitIndexEvent({ phase: 'begin', file: f, index: scannedSoFar - 1, total: files.length });
198
+ }
199
+ const full = path_1.default.join(dir, f);
200
+ // Recursion / governance denial: prevent ingestion of files that originate from
201
+ // repository governance or specification seed areas that must not become part of
202
+ // the live instruction index (avoids knowledge recursion loops).
203
+ // We rely on a simple fast path filename / parent folder heuristic here because
204
+ // governance artifacts are intentionally never written into the primary instructions
205
+ // directory. However, defensive hardening protects against accidental copy or user
206
+ // misplacement (e.g., copying specs/*.json into instructions/).
207
+ // Deny patterns (case-insensitive):
208
+ // - files whose basename starts with '000-bootstrapper' or '001-knowledge-index-lifecycle'
209
+ // - any file containing '.governance.' marker (future use)
210
+ // - any file named 'constitution.json'
211
+ // - any file whose first line (if readable) contains marker '__GOVERNANCE_SEED__'
212
+ const lowerBase = f.toLowerCase();
213
+ let denied = false;
214
+ if (/^(000-bootstrapper|001-lifecycle-bootstrap)/.test(lowerBase))
215
+ denied = true;
216
+ else if (lowerBase.includes('.governance.'))
217
+ denied = true;
218
+ else if (lowerBase === 'constitution.json')
219
+ denied = true;
220
+ if (!denied) {
221
+ try {
222
+ // Very small peek (first 200 bytes) – safe even for large files
223
+ const peek = fs_1.default.readFileSync(full, { encoding: 'utf8', flag: 'r' }).slice(0, 200);
224
+ if (/__GOVERNANCE_SEED__/.test(peek))
225
+ denied = true;
226
+ }
227
+ catch { /* ignore peek errors */ }
228
+ }
229
+ if (denied) {
230
+ bump('ignored:governance-denylist');
231
+ if (trace)
232
+ trace.push({ file: f, accepted: false, reason: 'ignored:governance-denylist' });
233
+ if ((0, tracing_1.traceEnabled)(1)) {
234
+ try {
235
+ (0, tracing_1.emitTrace)('[trace:index:file-end]', { file: f, accepted: false, reason: 'ignored:governance-denylist', scanned: scannedSoFar, acceptedSoFar });
236
+ }
237
+ catch { /* ignore */ }
238
+ try {
239
+ (0, tracing_1.emitTrace)('[trace:index:file-progress]', { scanned: scannedSoFar, total: files.length, acceptedSoFar, rejectedSoFar: scannedSoFar - acceptedSoFar });
240
+ }
241
+ catch { /* ignore */ }
242
+ }
243
+ else {
244
+ emitIndexEvent({ phase: 'end', file: f, accepted: false, reason: 'ignored:governance-denylist', scanned: scannedSoFar, acceptedSoFar });
245
+ }
246
+ continue;
247
+ }
248
+ // Attempt cache reuse before any I/O beyond stat
249
+ let reused = false;
250
+ if (memoryCacheEnabled) {
251
+ try {
252
+ const st = fs_1.default.statSync(full);
253
+ const cached = indexMemo.get(full);
254
+ if (cached && cached.size === st.size && Math.abs(cached.mtimeMs - st.mtimeMs) < 1 && cached.buildSig === buildSig) {
255
+ // Reuse cached normalized entry
256
+ entries.push({ ...cached.entry });
257
+ acceptedSoFar++;
258
+ cacheHits++;
259
+ reused = true;
260
+ if (trace)
261
+ trace.push({ file: f, accepted: true, reason: 'cache-hit' });
262
+ if ((0, tracing_1.traceEnabled)(1)) {
263
+ try {
264
+ (0, tracing_1.emitTrace)('[trace:index:file-end]', { file: f, accepted: true, cached: true, scanned: scannedSoFar, acceptedSoFar });
265
+ }
266
+ catch { /* ignore */ }
267
+ try {
268
+ (0, tracing_1.emitTrace)('[trace:index:file-progress]', { scanned: scannedSoFar, total: files.length, acceptedSoFar, rejectedSoFar: scannedSoFar - acceptedSoFar });
269
+ }
270
+ catch { /* ignore */ }
271
+ }
272
+ else {
273
+ emitIndexEvent({ phase: 'end', file: f, accepted: true, cached: true, scanned: scannedSoFar, acceptedSoFar });
274
+ }
275
+ }
276
+ }
277
+ catch { /* stat or reuse failure falls through to normal path */ }
278
+ }
279
+ if (reused)
280
+ continue;
281
+ // Exclude any files within a _templates directory (non-runtime placeholders)
282
+ if (full.includes(`${path_1.default.sep}_templates${path_1.default.sep}`)) {
283
+ bump('ignored:template');
284
+ if (trace)
285
+ trace.push({ file: f, accepted: false, reason: 'ignored:template' });
286
+ if ((0, tracing_1.traceEnabled)(1)) {
287
+ try {
288
+ (0, tracing_1.emitTrace)('[trace:index:file-end]', { file: f, accepted: false, reason: 'ignored:template', scanned: scannedSoFar, acceptedSoFar });
289
+ }
290
+ catch { /* ignore */ }
291
+ try {
292
+ (0, tracing_1.emitTrace)('[trace:index:file-progress]', { scanned: scannedSoFar, total: files.length, acceptedSoFar, rejectedSoFar: scannedSoFar - acceptedSoFar });
293
+ }
294
+ catch { /* ignore */ }
295
+ }
296
+ else {
297
+ emitIndexEvent({ phase: 'end', file: f, accepted: false, reason: 'ignored:template', scanned: scannedSoFar, acceptedSoFar });
298
+ }
299
+ continue;
300
+ }
301
+ try {
302
+ // Hash-based reuse path (only if metadata changed but content identical). We compute hash before parsing.
303
+ if (memoryCacheEnabled && hashMemoEnabled) {
304
+ try {
305
+ const rawBuf = fs_1.default.readFileSync(full);
306
+ const contentHash = crypto_1.default.createHash('sha256').update(rawBuf).digest('hex');
307
+ const cached = indexMemo.get(full);
308
+ if (cached && cached.contentHash === contentHash && cached.buildSig === buildSig) {
309
+ // Accept from hash cache without reparsing / revalidation
310
+ entries.push({ ...cached.entry });
311
+ acceptedSoFar++;
312
+ hashHits++;
313
+ if (trace)
314
+ trace.push({ file: f, accepted: true, reason: 'hash-cache-hit' });
315
+ if ((0, tracing_1.traceEnabled)(1)) {
316
+ try {
317
+ (0, tracing_1.emitTrace)('[trace:index:file-end]', { file: f, accepted: true, cached: true, hash: true, scanned: scannedSoFar, acceptedSoFar });
318
+ }
319
+ catch { /* ignore */ }
320
+ try {
321
+ (0, tracing_1.emitTrace)('[trace:index:file-progress]', { scanned: scannedSoFar, total: files.length, acceptedSoFar, rejectedSoFar: scannedSoFar - acceptedSoFar });
322
+ }
323
+ catch { /* ignore */ }
324
+ }
325
+ else {
326
+ emitIndexEvent({ phase: 'end', file: f, accepted: true, cached: true, hash: true, scanned: scannedSoFar, acceptedSoFar });
327
+ }
328
+ continue; // proceed next file
329
+ }
330
+ }
331
+ catch { /* fall through to normal parse */ }
332
+ }
333
+ const rawAny = this.readJsonWithRetry(full);
334
+ // Ignore clearly non-instruction config files (no id/title/body/requirement) e.g. gates.json
335
+ const looksInstruction = typeof rawAny.id === 'string' && typeof rawAny.title === 'string' && typeof rawAny.body === 'string';
336
+ if (!looksInstruction) {
337
+ bump('ignored:non-instruction-config');
338
+ if (trace)
339
+ trace.push({ file: f, accepted: false, reason: 'ignored:non-instruction-config' });
340
+ if ((0, tracing_1.traceEnabled)(1)) {
341
+ try {
342
+ (0, tracing_1.emitTrace)('[trace:index:file-end]', { file: f, accepted: false, reason: 'ignored:non-instruction-config', scanned: scannedSoFar, acceptedSoFar });
343
+ }
344
+ catch { /* ignore */ }
345
+ try {
346
+ (0, tracing_1.emitTrace)('[trace:index:file-progress]', { scanned: scannedSoFar, total: files.length, acceptedSoFar, rejectedSoFar: scannedSoFar - acceptedSoFar });
347
+ }
348
+ catch { /* ignore */ }
349
+ }
350
+ else {
351
+ emitIndexEvent({ phase: 'end', file: f, accepted: false, reason: 'ignored:non-instruction-config', scanned: scannedSoFar, acceptedSoFar });
352
+ }
353
+ continue;
354
+ }
355
+ // Clone to typed object after basic shape check
356
+ const raw = rawAny; // validated progressively below
357
+ const preSnapshot = {
358
+ id: raw.id,
359
+ audience: raw.audience,
360
+ requirement: raw.requirement,
361
+ categories: Array.isArray(raw.categories) ? [...raw.categories] : undefined,
362
+ primaryCategory: raw.primaryCategory
363
+ };
364
+ // Check schema version and migrate if needed
365
+ let needsRewrite = false;
366
+ if (!raw.schemaVersion || raw.schemaVersion !== schemaVersion_1.SCHEMA_VERSION) {
367
+ const mig = (0, schemaVersion_1.migrateInstructionRecord)(raw);
368
+ if (mig.changed) {
369
+ needsRewrite = true;
370
+ }
371
+ }
372
+ if (needsRewrite) {
373
+ try {
374
+ fs_1.default.writeFileSync(full, JSON.stringify(raw, null, 2));
375
+ }
376
+ catch { /* ignore rewrite failure */ }
377
+ }
378
+ const mutRaw = raw;
379
+ // Derive minimal required fields for backward compatibility (new relaxed schema allows missing governance fields)
380
+ const nowIso = new Date().toISOString();
381
+ if (typeof mutRaw.sourceHash !== 'string' || !mutRaw.sourceHash.length) {
382
+ try {
383
+ mutRaw.sourceHash = crypto_1.default.createHash('sha256').update(mutRaw.body || '', 'utf8').digest('hex');
384
+ }
385
+ catch { /* ignore */ }
386
+ }
387
+ if (typeof mutRaw.createdAt !== 'string' || !mutRaw.createdAt.length)
388
+ mutRaw.createdAt = nowIso;
389
+ if (typeof mutRaw.updatedAt !== 'string' || !mutRaw.updatedAt.length)
390
+ mutRaw.updatedAt = nowIso;
391
+ // Normalize legacy / pre-spec governance variants BEFORE schema validation
392
+ // Type guard: raw.status may contain legacy value 'active'; treat as alias for 'approved'
393
+ if (raw.status === 'active') {
394
+ raw.status = 'approved';
395
+ }
396
+ // BEGIN legacy enum normalization (no flags, always-on). This upgrades historical values to v3 schema enums.
397
+ try {
398
+ const legacyAudienceMap = {
399
+ 'system': 'all', // legacy broad scope
400
+ 'developers': 'group',
401
+ 'developer': 'individual',
402
+ 'team': 'group',
403
+ 'teams': 'group',
404
+ 'users': 'group',
405
+ 'dev': 'individual',
406
+ 'devs': 'group',
407
+ // newly observed variants
408
+ 'testers': 'group',
409
+ 'administrators': 'group',
410
+ 'admins': 'group',
411
+ 'agents': 'group',
412
+ 'powershell script authors': 'group'
413
+ };
414
+ const legacyRequirementMap = {
415
+ 'MUST': 'mandatory',
416
+ 'SHOULD': 'recommended',
417
+ 'MAY': 'optional',
418
+ 'CRITICAL': 'critical', // legacy capitalization variant
419
+ 'OPTIONAL': 'optional',
420
+ 'MANDATORY': 'mandatory',
421
+ 'DEPRECATED': 'deprecated',
422
+ // newly observed variant
423
+ 'REQUIRED': 'mandatory'
424
+ };
425
+ let changedLegacy = false;
426
+ const anyMut = mutRaw;
427
+ if (typeof anyMut.audience === 'string') {
428
+ const lower = anyMut.audience.toLowerCase();
429
+ // try exact key, then lower-case key
430
+ if (legacyAudienceMap[anyMut.audience]) {
431
+ anyMut.audience = legacyAudienceMap[anyMut.audience];
432
+ changedLegacy = true;
433
+ salvage('audience');
434
+ }
435
+ else if (legacyAudienceMap[lower]) {
436
+ anyMut.audience = legacyAudienceMap[lower];
437
+ changedLegacy = true;
438
+ salvage('audience');
439
+ }
440
+ else if (/author|script\s+author/i.test(lower)) {
441
+ anyMut.audience = 'individual';
442
+ changedLegacy = true;
443
+ salvage('audience');
444
+ }
445
+ }
446
+ if (typeof anyMut.requirement === 'string') {
447
+ const req = anyMut.requirement;
448
+ if (legacyRequirementMap[req]) {
449
+ anyMut.requirement = legacyRequirementMap[req];
450
+ changedLegacy = true;
451
+ salvage('requirement');
452
+ }
453
+ else {
454
+ const upper = req.toUpperCase();
455
+ if (legacyRequirementMap[upper]) {
456
+ anyMut.requirement = legacyRequirementMap[upper];
457
+ changedLegacy = true;
458
+ salvage('requirement');
459
+ }
460
+ else if (/\s/.test(req) && req.length < 300) {
461
+ // Free-form sentence / description -> degrade to recommended (heuristic)
462
+ anyMut.requirement = 'recommended';
463
+ changedLegacy = true;
464
+ salvage('requirementFreeform');
465
+ }
466
+ }
467
+ }
468
+ // Category + primaryCategory sanitization: lowercase, remove invalid chars, collapse multiple separators
469
+ const sanitizeCat = (v) => {
470
+ let out = v.toLowerCase().trim();
471
+ out = out.replace(/[^a-z0-9-_]/g, '-'); // replace invalid with '-'
472
+ out = out.replace(/-{2,}/g, '-').replace(/_{2,}/g, '_');
473
+ out = out.replace(/^-+/, '').replace(/-+$/, '');
474
+ out = out.slice(0, 49);
475
+ if (!out.match(/^[a-z0-9][a-z0-9-_]*$/)) {
476
+ // fallback id-like token if still invalid
477
+ out = out.replace(/^[^a-z0-9]+/, '');
478
+ if (!out)
479
+ out = 'uncategorized';
480
+ }
481
+ return out;
482
+ };
483
+ const mutPartial = anyMut;
484
+ if (Array.isArray(mutPartial.categories)) {
485
+ const before = JSON.stringify(mutPartial.categories);
486
+ mutPartial.categories = mutPartial.categories
487
+ .filter((c) => typeof c === 'string')
488
+ .map((c) => sanitizeCat(c))
489
+ .filter((c, idx, arr) => c.length && arr.indexOf(c) === idx)
490
+ .slice(0, 25);
491
+ if (before !== JSON.stringify(mutPartial.categories))
492
+ changedLegacy = true;
493
+ }
494
+ if (typeof mutPartial.primaryCategory === 'string') {
495
+ const pc = sanitizeCat(mutPartial.primaryCategory);
496
+ if (pc !== mutPartial.primaryCategory) {
497
+ mutPartial.primaryCategory = pc;
498
+ changedLegacy = true;
499
+ }
500
+ // ensure membership
501
+ if (Array.isArray(mutPartial.categories) && !mutPartial.categories.includes(pc)) {
502
+ mutPartial.categories.push(pc);
503
+ changedLegacy = true;
504
+ }
505
+ }
506
+ // ID sanitization (only if currently invalid but has recognizable content). Avoid changing valid ids.
507
+ if (typeof anyMut.id === 'string') {
508
+ if (!/^[a-z0-9](?:[a-z0-9-_]{0,118}[a-z0-9])?$/.test(anyMut.id)) {
509
+ const orig = anyMut.id;
510
+ let id = orig.toLowerCase().trim();
511
+ id = id.replace(/[^a-z0-9-_]/g, '-');
512
+ id = id.replace(/-{2,}/g, '-').replace(/_{2,}/g, '_');
513
+ id = id.replace(/^-+/, '').replace(/-+$/, '');
514
+ id = id.slice(0, 120);
515
+ if (id && /^[a-z0-9]/.test(id) && /[a-z0-9]$/.test(id)) {
516
+ anyMut.id = id;
517
+ changedLegacy = true;
518
+ }
519
+ }
520
+ }
521
+ if (changedLegacy) {
522
+ needsRewrite = true; // ensure persisted normalization
523
+ try {
524
+ // Produce diff (post-normalization snapshot limited to mutated fields)
525
+ const postSnapshot = {
526
+ id: anyMut.id,
527
+ audience: anyMut.audience,
528
+ requirement: anyMut.requirement,
529
+ categories: Array.isArray(anyMut.categories) ? [...anyMut.categories] : undefined,
530
+ primaryCategory: anyMut.primaryCategory
531
+ };
532
+ const changed = {};
533
+ for (const k of Object.keys(postSnapshot)) {
534
+ const beforeVal = preSnapshot[k];
535
+ const afterVal = postSnapshot[k];
536
+ const beforeJSON = JSON.stringify(beforeVal);
537
+ const afterJSON = JSON.stringify(afterVal);
538
+ if (beforeJSON !== afterJSON) {
539
+ changed[k] = { before: beforeVal, after: afterVal };
540
+ }
541
+ }
542
+ if (Object.keys(changed).length) {
543
+ normLogRecords.push({
544
+ ts: new Date().toISOString(),
545
+ file: f,
546
+ originalId: preSnapshot.id,
547
+ finalId: anyMut.id,
548
+ changes: changed
549
+ });
550
+ }
551
+ }
552
+ catch { /* ignore diff / logging errors */ }
553
+ }
554
+ }
555
+ catch { /* swallow normalization failure */ }
556
+ // END legacy enum normalization
557
+ // Preprocess placeholder governance fields: convert empty strings to undefined so schema doesn't reject
558
+ const placeholderKeys = ['createdAt', 'updatedAt', 'lastReviewedAt', 'nextReviewDue', 'priorityTier', 'semanticSummary'];
559
+ for (const k of placeholderKeys) {
560
+ const v = mutRaw[k];
561
+ if (v === '') {
562
+ delete mutRaw[k];
563
+ }
564
+ }
565
+ // Soft warning for large body approaching limit ( >90% of bodyMaxLen )
566
+ try {
567
+ const nearLimitThreshold = Math.floor(bodyMaxLen * 0.9);
568
+ if (typeof mutRaw.body === 'string' && mutRaw.body.length > nearLimitThreshold && mutRaw.body.length <= bodyMaxLen) {
569
+ warnSoft('body:near-limit');
570
+ }
571
+ }
572
+ catch { /* ignore */ }
573
+ // Pre-schema salvage of clearly invalid enum values (post legacy normalization). This converts would-be
574
+ // schema rejections into accepted entries with a salvage marker to prevent noisy drift in environments
575
+ // where upstream authoring tools haven't yet been updated. These rules are intentionally conservative.
576
+ try {
577
+ const mi = mutRaw;
578
+ // riskScore is optional but must be a number if present — null values from
579
+ // upstream authoring tools (audit reports, bootstrap snapshots) cause AJV
580
+ // rejection. Delete null so the field reverts to absent (valid for optional).
581
+ if ('riskScore' in mi && (mi.riskScore === null || mi.riskScore === undefined)) {
582
+ delete mi.riskScore;
583
+ salvage('riskScoreNull');
584
+ }
585
+ if (mi) {
586
+ if (typeof mi.audience !== 'string' || !mi.audience) {
587
+ mi.audience = 'all';
588
+ salvage('audienceMissing');
589
+ }
590
+ else if (!['individual', 'group', 'all'].includes(mi.audience)) {
591
+ mi.audience = 'all';
592
+ salvage('audienceInvalid');
593
+ }
594
+ if (typeof mi.requirement !== 'string' || !mi.requirement) {
595
+ mi.requirement = 'recommended';
596
+ salvage('requirementMissing');
597
+ }
598
+ else if (!['mandatory', 'critical', 'recommended', 'optional', 'deprecated'].includes(mi.requirement)) {
599
+ mi.requirement = 'recommended';
600
+ salvage('requirementInvalid');
601
+ }
602
+ if (typeof mi.priority !== 'number' || mi.priority < 1 || mi.priority > 100) {
603
+ mi.priority = 50;
604
+ salvage('priorityInvalid');
605
+ }
606
+ if (typeof mi.priorityTier === 'string' && !['P1', 'P2', 'P3', 'P4'].includes(mi.priorityTier)) {
607
+ delete mi.priorityTier;
608
+ salvage('priorityTierInvalid');
609
+ }
610
+ if (typeof mi.body === 'string' && mi.body.length > bodyMaxLen) {
611
+ if (autoSplit) {
612
+ // Auto-split: split the oversized entry into cross-linked parts,
613
+ // write part files to disk, archive the original, and load parts
614
+ // instead of the truncated original.
615
+ try {
616
+ const splitParts = (0, autoSplit_1.splitOversizedEntry)(mi, bodyMaxLen);
617
+ if (splitParts.length > 1) {
618
+ // Write each part to disk
619
+ for (const part of splitParts) {
620
+ const partFile = path_1.default.join(dir, `${part.id}.json`);
621
+ fs_1.default.writeFileSync(partFile, JSON.stringify(part, null, 2), 'utf8');
622
+ }
623
+ // Archive the original file
624
+ fs_1.default.renameSync(full, full + '.archived');
625
+ salvage('bodySplit');
626
+ // Validate and push each split part
627
+ for (const part of splitParts) {
628
+ const partRaw = JSON.parse(JSON.stringify(part));
629
+ if (validate(partRaw)) {
630
+ const partNormalized = this.classifier.normalize(partRaw);
631
+ entries.push(partNormalized);
632
+ acceptedSoFar++;
633
+ }
634
+ }
635
+ if ((0, tracing_1.traceEnabled)(1)) {
636
+ try {
637
+ (0, tracing_1.emitTrace)('[trace:index:file-end]', { file: f, accepted: true, reason: 'auto-split', parts: splitParts.length, scanned: scannedSoFar, acceptedSoFar });
638
+ }
639
+ catch { /* ignore */ }
640
+ }
641
+ continue; // skip normal single-entry path for this file
642
+ }
643
+ }
644
+ catch { /* fall through to truncation if split fails */ }
645
+ }
646
+ mi.body = mi.body.slice(0, bodyMaxLen);
647
+ salvage('bodyTruncated');
648
+ warnSoft('body:truncated');
649
+ }
650
+ }
651
+ }
652
+ catch { /* salvage is best-effort */ }
653
+ if (!validate(mutRaw)) {
654
+ const ajvErrs = validate.errors;
655
+ let detailed = '';
656
+ if (Array.isArray(ajvErrs) && ajvErrs.length) {
657
+ detailed = ajvErrs.map(e => {
658
+ const path = e.instancePath && e.instancePath.length ? e.instancePath : '(root)';
659
+ let msg = e.message || 'invalid';
660
+ if (e.params && typeof e.params.allowedValues !== 'undefined') {
661
+ msg += ` allowed: ${JSON.stringify(e.params.allowedValues)}`;
662
+ }
663
+ return `${path}: ${msg}`;
664
+ }).join('; ');
665
+ }
666
+ else {
667
+ detailed = ajv.errorsText();
668
+ }
669
+ const reason = 'schema: ' + (detailed || 'validation failed');
670
+ errors.push({ file: f, error: reason });
671
+ bump('schema');
672
+ if (trace)
673
+ trace.push({ file: f, accepted: false, reason });
674
+ // Log schema rejections at info level for operational visibility
675
+ try {
676
+ console.error(`[index:skip] ${f}: ${reason}`);
677
+ }
678
+ catch { /* ignore */ }
679
+ if ((0, tracing_1.traceEnabled)(1)) {
680
+ try {
681
+ (0, tracing_1.emitTrace)('[trace:index:file-end]', { file: f, accepted: false, reason, scanned: scannedSoFar, acceptedSoFar });
682
+ }
683
+ catch { /* ignore */ }
684
+ try {
685
+ (0, tracing_1.emitTrace)('[trace:index:file-progress]', { scanned: scannedSoFar, total: files.length, acceptedSoFar, rejectedSoFar: scannedSoFar - acceptedSoFar });
686
+ }
687
+ catch { /* ignore */ }
688
+ }
689
+ else {
690
+ emitIndexEvent({ phase: 'end', file: f, accepted: false, reason, scanned: scannedSoFar, acceptedSoFar });
691
+ }
692
+ continue;
693
+ }
694
+ const issues = this.classifier.validate(mutRaw);
695
+ if (issues.length) {
696
+ const reason = issues.join(', ');
697
+ errors.push({ file: f, error: reason });
698
+ bump('classification');
699
+ if (trace)
700
+ trace.push({ file: f, accepted: false, reason });
701
+ try {
702
+ console.error(`[index:skip] ${f}: classification: ${reason}`);
703
+ }
704
+ catch { /* ignore */ }
705
+ if ((0, tracing_1.traceEnabled)(1)) {
706
+ try {
707
+ (0, tracing_1.emitTrace)('[trace:index:file-end]', { file: f, accepted: false, reason, scanned: scannedSoFar, acceptedSoFar });
708
+ }
709
+ catch { /* ignore */ }
710
+ try {
711
+ (0, tracing_1.emitTrace)('[trace:index:file-progress]', { scanned: scannedSoFar, total: files.length, acceptedSoFar, rejectedSoFar: scannedSoFar - acceptedSoFar });
712
+ }
713
+ catch { /* ignore */ }
714
+ }
715
+ else {
716
+ emitIndexEvent({ phase: 'end', file: f, accepted: false, reason, scanned: scannedSoFar, acceptedSoFar });
717
+ }
718
+ continue;
719
+ }
720
+ const normalized = this.classifier.normalize(mutRaw);
721
+ entries.push(normalized);
722
+ acceptedSoFar++;
723
+ // Populate / refresh cache after successful normalization
724
+ if (memoryCacheEnabled) {
725
+ try {
726
+ const st = fs_1.default.statSync(full);
727
+ let contentHash;
728
+ if (hashMemoEnabled) {
729
+ try {
730
+ contentHash = crypto_1.default.createHash('sha256').update(JSON.stringify(normalized)).digest('hex');
731
+ }
732
+ catch { /* ignore */ }
733
+ }
734
+ indexMemo.set(full, { mtimeMs: st.mtimeMs, size: st.size, entry: { ...normalized }, contentHash, buildSig });
735
+ }
736
+ catch { /* ignore */ }
737
+ }
738
+ if (trace)
739
+ trace.push({ file: f, accepted: true });
740
+ if ((0, tracing_1.traceEnabled)(1)) {
741
+ try {
742
+ (0, tracing_1.emitTrace)('[trace:index:file-end]', { file: f, accepted: true, scanned: scannedSoFar, acceptedSoFar });
743
+ }
744
+ catch { /* ignore */ }
745
+ try {
746
+ (0, tracing_1.emitTrace)('[trace:index:file-progress]', { scanned: scannedSoFar, total: files.length, acceptedSoFar, rejectedSoFar: scannedSoFar - acceptedSoFar });
747
+ }
748
+ catch { /* ignore */ }
749
+ }
750
+ else {
751
+ emitIndexEvent({ phase: 'end', file: f, accepted: true, scanned: scannedSoFar, acceptedSoFar });
752
+ }
753
+ }
754
+ catch (e) {
755
+ const reason = e instanceof Error ? e.message : 'unknown error';
756
+ errors.push({ file: f, error: reason });
757
+ bump('error');
758
+ if (trace)
759
+ trace.push({ file: f, accepted: false, reason });
760
+ if ((0, tracing_1.traceEnabled)(1)) {
761
+ try {
762
+ (0, tracing_1.emitTrace)('[trace:index:file-end]', { file: f, accepted: false, reason, scanned: scannedSoFar, acceptedSoFar });
763
+ }
764
+ catch { /* ignore */ }
765
+ try {
766
+ (0, tracing_1.emitTrace)('[trace:index:file-progress]', { scanned: scannedSoFar, total: files.length, acceptedSoFar, rejectedSoFar: scannedSoFar - acceptedSoFar });
767
+ }
768
+ catch { /* ignore */ }
769
+ }
770
+ else {
771
+ emitIndexEvent({ phase: 'end', file: f, accepted: false, reason, scanned: scannedSoFar, acceptedSoFar });
772
+ }
773
+ }
774
+ }
775
+ const hash = this.computeindexHash(entries);
776
+ const summary = {
777
+ scanned: files.length,
778
+ accepted: entries.length,
779
+ skipped: files.length - entries.length,
780
+ reasons: reasonCounts,
781
+ cacheHits: cacheHits || undefined,
782
+ hashHits: hashHits || undefined,
783
+ salvage: Object.keys(salvageCounts).length ? salvageCounts : undefined,
784
+ softWarnings: Object.keys(softWarnings).length ? softWarnings : undefined
785
+ };
786
+ // Always emit a single-line summary to stderr for deterministic external diagnostics.
787
+ try {
788
+ const line = JSON.stringify({ level: 'info', event: 'index-summary', ...summary });
789
+ process.stderr.write(line + '\n');
790
+ }
791
+ catch { /* ignore logging failure */ }
792
+ // Generate manifest file with accepted entries + summary for external tooling and validate against schema.
793
+ try {
794
+ const manifestEntries = entries.map(e => {
795
+ let bodyHash = '';
796
+ try {
797
+ bodyHash = crypto_1.default.createHash('sha256').update(e.body, 'utf8').digest('hex');
798
+ }
799
+ catch { /* ignore */ }
800
+ return {
801
+ id: e.id,
802
+ title: e.title,
803
+ priority: e.priority,
804
+ priorityTier: e.priorityTier,
805
+ audience: e.audience,
806
+ requirement: e.requirement,
807
+ sourceHash: e.sourceHash,
808
+ bodyHash
809
+ };
810
+ });
811
+ const manifest = {
812
+ version: 1,
813
+ generatedAt: new Date().toISOString(),
814
+ count: manifestEntries.length,
815
+ hash,
816
+ summary,
817
+ entries: manifestEntries
818
+ };
819
+ // Validate manifest against schema (best-effort, non-fatal)
820
+ try {
821
+ const manifestSchemaPath = path_1.default.join(process.cwd(), 'schemas', 'manifest.schema.json');
822
+ if (fs_1.default.existsSync(manifestSchemaPath)) {
823
+ try {
824
+ const sRaw = fs_1.default.readFileSync(manifestSchemaPath, 'utf8');
825
+ const sJson = JSON.parse(sRaw);
826
+ const ajvManifest = new ajv_1.default({ allErrors: true, strict: false });
827
+ (0, ajv_formats_1.default)(ajvManifest);
828
+ const validateManifest = ajvManifest.compile(sJson);
829
+ if (!validateManifest(manifest)) {
830
+ const errs = validateManifest.errors?.map(e => `${e.instancePath || '(root)'} ${e.message}`).join('; ');
831
+ const line = JSON.stringify({ level: 'warn', event: 'index-manifest-validation-failed', errors: errs });
832
+ process.stderr.write(line + '\n');
833
+ }
834
+ }
835
+ catch { /* ignore schema validation errors */ }
836
+ }
837
+ }
838
+ catch { /* ignore validation wrapper errors */ }
839
+ const manifestPath = path_1.default.join(dir, MANIFEST_NAME);
840
+ const tmpPath = manifestPath + '.tmp';
841
+ fs_1.default.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2));
842
+ fs_1.default.renameSync(tmpPath, manifestPath); // atomic replace
843
+ }
844
+ catch { /* ignore manifest failure */ }
845
+ // Emit skipped details artifact (_skipped.json) for transparency
846
+ try {
847
+ const skipped = errors.map(e => ({ file: e.file, reason: e.error }));
848
+ const skippedPath = path_1.default.join(dir, '_skipped.json');
849
+ const tmpSkipped = skippedPath + '.tmp';
850
+ const payload = { generatedAt: new Date().toISOString(), count: skipped.length, items: skipped };
851
+ fs_1.default.writeFileSync(tmpSkipped, JSON.stringify(payload, null, 2));
852
+ fs_1.default.renameSync(tmpSkipped, skippedPath);
853
+ }
854
+ catch { /* ignore skipped artifact failure */ }
855
+ // Emit normalization audit log if enabled and we have records
856
+ if (normLogTarget && normLogRecords.length) {
857
+ try {
858
+ const target = normLogTarget;
859
+ const targetDir = path_1.default.dirname(target);
860
+ if (!fs_1.default.existsSync(targetDir)) {
861
+ try {
862
+ fs_1.default.mkdirSync(targetDir, { recursive: true });
863
+ }
864
+ catch { /* ignore */ }
865
+ }
866
+ const fd = fs_1.default.openSync(target, 'a');
867
+ try {
868
+ for (const rec of normLogRecords) {
869
+ fs_1.default.writeSync(fd, JSON.stringify(rec) + '\n');
870
+ }
871
+ }
872
+ finally {
873
+ try {
874
+ fs_1.default.closeSync(fd);
875
+ }
876
+ catch { /* ignore */ }
877
+ }
878
+ }
879
+ catch { /* silent failure */ }
880
+ }
881
+ if (memoryCacheEnabled && (cacheHits || hashHits) && (0, tracing_1.traceEnabled)(1)) {
882
+ try {
883
+ (0, tracing_1.emitTrace)('[trace:index:cache-summary]', { hits: cacheHits, hashHits, scanned: files.length, percent: Number(((cacheHits + hashHits) / files.length * 100).toFixed(2)) });
884
+ }
885
+ catch { /* ignore */ }
886
+ }
887
+ const loadDurationMs = Date.now() - loadStart;
888
+ if ((0, tracing_1.traceEnabled)(1)) {
889
+ try {
890
+ (0, tracing_1.emitTrace)('[trace:index:load-end]', { dir, durationMs: loadDurationMs, accepted: entries.length, skipped: files.length - entries.length });
891
+ }
892
+ catch { /* ignore */ }
893
+ }
894
+ // Warn if index load time exceeds threshold (INDEX_SERVER_LOAD_WARN_MS)
895
+ const loadWarningThreshold = IndexConfig.loadWarningThreshold;
896
+ if (loadWarningThreshold && loadWarningThreshold > 0 && loadDurationMs > loadWarningThreshold) {
897
+ const line = JSON.stringify({
898
+ level: 'warn',
899
+ event: 'index-load-slow',
900
+ durationMs: loadDurationMs,
901
+ threshold: loadWarningThreshold,
902
+ filesScanned: files.length,
903
+ filesAccepted: entries.length,
904
+ message: `index load took ${loadDurationMs}ms (threshold: ${loadWarningThreshold}ms). Consider enabling memoization (INDEX_SERVER_MEMOIZE=1) or reducing index size.`
905
+ });
906
+ process.stderr.write(line + '\n');
907
+ }
908
+ return { entries, errors, hash, debug: { scanned: files.length, accepted: entries.length, skipped: files.length - entries.length, trace }, summary };
909
+ }
910
+ computeindexHash(entries) {
911
+ const h = crypto_1.default.createHash('sha256');
912
+ const stable = entries
913
+ .slice()
914
+ .sort((a, b) => a.id.localeCompare(b.id))
915
+ .map(e => `${e.id}:${e.sourceHash}`)
916
+ .join('|');
917
+ h.update(stable, 'utf8');
918
+ return h.digest('hex');
919
+ }
920
+ }
921
+ exports.IndexLoader = IndexLoader;