@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.
- package/CHANGELOG.md +1218 -0
- package/CODE_OF_CONDUCT.md +49 -0
- package/CONTRIBUTING.md +75 -0
- package/LICENSE +21 -0
- package/README.md +523 -0
- package/SECURITY.md +50 -0
- package/dist/config/configUtils.d.ts +11 -0
- package/dist/config/configUtils.js +87 -0
- package/dist/config/dashboardConfig.d.ts +45 -0
- package/dist/config/dashboardConfig.js +63 -0
- package/dist/config/defaultValues.d.ts +61 -0
- package/dist/config/defaultValues.js +70 -0
- package/dist/config/dirConstants.d.ts +17 -0
- package/dist/config/dirConstants.js +28 -0
- package/dist/config/featureConfig.d.ts +61 -0
- package/dist/config/featureConfig.js +121 -0
- package/dist/config/runtimeConfig.d.ts +145 -0
- package/dist/config/runtimeConfig.js +334 -0
- package/dist/config/serverConfig.d.ts +90 -0
- package/dist/config/serverConfig.js +164 -0
- package/dist/dashboard/analytics/AnalyticsEngine.d.ts +142 -0
- package/dist/dashboard/analytics/AnalyticsEngine.js +373 -0
- package/dist/dashboard/analytics/BusinessIntelligence.d.ts +187 -0
- package/dist/dashboard/analytics/BusinessIntelligence.js +594 -0
- package/dist/dashboard/client/admin.html +2150 -0
- package/dist/dashboard/client/chunks/mermaid-layout-elk.esm.min/chunk-SP2CHFBE.mjs +1 -0
- package/dist/dashboard/client/chunks/mermaid-layout-elk.esm.min/render-T6MDALS3.mjs +27 -0
- package/dist/dashboard/client/css/admin.css +1466 -0
- package/dist/dashboard/client/js/admin.boot.js +359 -0
- package/dist/dashboard/client/js/admin.config.js +196 -0
- package/dist/dashboard/client/js/admin.embeddings.js +425 -0
- package/dist/dashboard/client/js/admin.graph.js +583 -0
- package/dist/dashboard/client/js/admin.instances.js +120 -0
- package/dist/dashboard/client/js/admin.instructions.js +552 -0
- package/dist/dashboard/client/js/admin.logs.js +113 -0
- package/dist/dashboard/client/js/admin.maintenance.js +354 -0
- package/dist/dashboard/client/js/admin.messaging.js +635 -0
- package/dist/dashboard/client/js/admin.monitor.js +181 -0
- package/dist/dashboard/client/js/admin.overview.js +221 -0
- package/dist/dashboard/client/js/admin.performance.js +61 -0
- package/dist/dashboard/client/js/admin.sessions.js +293 -0
- package/dist/dashboard/client/js/admin.sqlite.js +366 -0
- package/dist/dashboard/client/js/admin.utils.js +49 -0
- package/dist/dashboard/client/js/chart.umd.js +14 -0
- package/dist/dashboard/client/js/elk.bundled.js +6696 -0
- package/dist/dashboard/client/js/marked.umd.js +74 -0
- package/dist/dashboard/client/js/mermaid.min.js +3022 -0
- package/dist/dashboard/client/mermaid-layout-elk.esm.min.mjs +1 -0
- package/dist/dashboard/export/DataExporter.d.ts +169 -0
- package/dist/dashboard/export/DataExporter.js +737 -0
- package/dist/dashboard/export/exporters/csvExporter.d.ts +11 -0
- package/dist/dashboard/export/exporters/csvExporter.js +46 -0
- package/dist/dashboard/export/exporters/exportTypes.d.ts +89 -0
- package/dist/dashboard/export/exporters/exportTypes.js +5 -0
- package/dist/dashboard/export/exporters/jsonExporter.d.ts +7 -0
- package/dist/dashboard/export/exporters/jsonExporter.js +22 -0
- package/dist/dashboard/export/exporters/xmlExporter.d.ts +17 -0
- package/dist/dashboard/export/exporters/xmlExporter.js +175 -0
- package/dist/dashboard/integration/APIIntegration.d.ts +41 -0
- package/dist/dashboard/integration/APIIntegration.js +95 -0
- package/dist/dashboard/security/SecurityMonitor.d.ts +167 -0
- package/dist/dashboard/security/SecurityMonitor.js +559 -0
- package/dist/dashboard/server/AdminPanel.d.ts +183 -0
- package/dist/dashboard/server/AdminPanel.js +792 -0
- package/dist/dashboard/server/AdminPanelConfig.d.ts +42 -0
- package/dist/dashboard/server/AdminPanelConfig.js +80 -0
- package/dist/dashboard/server/AdminPanelState.d.ts +47 -0
- package/dist/dashboard/server/AdminPanelState.js +214 -0
- package/dist/dashboard/server/ApiRoutes.d.ts +17 -0
- package/dist/dashboard/server/ApiRoutes.js +149 -0
- package/dist/dashboard/server/DashboardServer.d.ts +49 -0
- package/dist/dashboard/server/DashboardServer.js +159 -0
- package/dist/dashboard/server/FileMetricsStorage.d.ts +49 -0
- package/dist/dashboard/server/FileMetricsStorage.js +195 -0
- package/dist/dashboard/server/HttpTransport.d.ts +23 -0
- package/dist/dashboard/server/HttpTransport.js +116 -0
- package/dist/dashboard/server/InstanceManager.d.ts +53 -0
- package/dist/dashboard/server/InstanceManager.js +284 -0
- package/dist/dashboard/server/KnowledgeStore.d.ts +35 -0
- package/dist/dashboard/server/KnowledgeStore.js +105 -0
- package/dist/dashboard/server/LeaderElection.d.ts +81 -0
- package/dist/dashboard/server/LeaderElection.js +268 -0
- package/dist/dashboard/server/MetricsCollector.d.ts +200 -0
- package/dist/dashboard/server/MetricsCollector.js +803 -0
- package/dist/dashboard/server/SessionPersistenceManager.d.ts +88 -0
- package/dist/dashboard/server/SessionPersistenceManager.js +457 -0
- package/dist/dashboard/server/ThinClient.d.ts +64 -0
- package/dist/dashboard/server/ThinClient.js +237 -0
- package/dist/dashboard/server/WebSocketManager.d.ts +161 -0
- package/dist/dashboard/server/WebSocketManager.js +463 -0
- package/dist/dashboard/server/httpLifecycle.d.ts +17 -0
- package/dist/dashboard/server/httpLifecycle.js +35 -0
- package/dist/dashboard/server/legacyDashboardHtml.d.ts +9 -0
- package/dist/dashboard/server/legacyDashboardHtml.js +618 -0
- package/dist/dashboard/server/legacyDashboardStyles.d.ts +5 -0
- package/dist/dashboard/server/legacyDashboardStyles.js +490 -0
- package/dist/dashboard/server/metricsAggregation.d.ts +252 -0
- package/dist/dashboard/server/metricsAggregation.js +206 -0
- package/dist/dashboard/server/metricsSerializer.d.ts +25 -0
- package/dist/dashboard/server/metricsSerializer.js +195 -0
- package/dist/dashboard/server/routes/admin.routes.d.ts +16 -0
- package/dist/dashboard/server/routes/admin.routes.js +596 -0
- package/dist/dashboard/server/routes/alerts.routes.d.ts +7 -0
- package/dist/dashboard/server/routes/alerts.routes.js +93 -0
- package/dist/dashboard/server/routes/api.feedback.routes.d.ts +73 -0
- package/dist/dashboard/server/routes/api.feedback.routes.js +171 -0
- package/dist/dashboard/server/routes/api.instructions.routes.d.ts +101 -0
- package/dist/dashboard/server/routes/api.instructions.routes.js +213 -0
- package/dist/dashboard/server/routes/api.usage.routes.d.ts +57 -0
- package/dist/dashboard/server/routes/api.usage.routes.js +374 -0
- package/dist/dashboard/server/routes/embeddings.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/embeddings.routes.js +246 -0
- package/dist/dashboard/server/routes/graph.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/graph.routes.js +280 -0
- package/dist/dashboard/server/routes/index.d.ts +38 -0
- package/dist/dashboard/server/routes/index.js +194 -0
- package/dist/dashboard/server/routes/instances.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/instances.routes.js +35 -0
- package/dist/dashboard/server/routes/instructions.routes.d.ts +8 -0
- package/dist/dashboard/server/routes/instructions.routes.js +336 -0
- package/dist/dashboard/server/routes/knowledge.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/knowledge.routes.js +82 -0
- package/dist/dashboard/server/routes/logs.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/logs.routes.js +164 -0
- package/dist/dashboard/server/routes/messaging.routes.d.ts +16 -0
- package/dist/dashboard/server/routes/messaging.routes.js +293 -0
- package/dist/dashboard/server/routes/metrics.routes.d.ts +10 -0
- package/dist/dashboard/server/routes/metrics.routes.js +346 -0
- package/dist/dashboard/server/routes/scripts.routes.d.ts +9 -0
- package/dist/dashboard/server/routes/scripts.routes.js +84 -0
- package/dist/dashboard/server/routes/sqlite.routes.d.ts +9 -0
- package/dist/dashboard/server/routes/sqlite.routes.js +569 -0
- package/dist/dashboard/server/routes/status.routes.d.ts +7 -0
- package/dist/dashboard/server/routes/status.routes.js +183 -0
- package/dist/dashboard/server/routes/synthetic.routes.d.ts +7 -0
- package/dist/dashboard/server/routes/synthetic.routes.js +195 -0
- package/dist/dashboard/server/routes/tools.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/tools.routes.js +46 -0
- package/dist/dashboard/server/routes/usage.routes.d.ts +6 -0
- package/dist/dashboard/server/routes/usage.routes.js +25 -0
- package/dist/dashboard/server/wsInit.d.ts +16 -0
- package/dist/dashboard/server/wsInit.js +35 -0
- package/dist/externalClientLib.d.ts +1 -0
- package/dist/externalClientLib.js +2 -0
- package/dist/minimal/index.d.ts +1 -0
- package/dist/minimal/index.js +140 -0
- package/dist/models/SessionPersistence.d.ts +115 -0
- package/dist/models/SessionPersistence.js +66 -0
- package/dist/models/instruction.d.ts +45 -0
- package/dist/models/instruction.js +2 -0
- package/dist/perf/benchmark.d.ts +1 -0
- package/dist/perf/benchmark.js +50 -0
- package/dist/portableClientWrapper.d.ts +1 -0
- package/dist/portableClientWrapper.js +2 -0
- package/dist/schemas/index.d.ts +128 -0
- package/dist/schemas/index.js +371 -0
- package/dist/scripts/runPerformanceBaseline.d.ts +1 -0
- package/dist/scripts/runPerformanceBaseline.js +17 -0
- package/dist/server/handshakeManager.d.ts +25 -0
- package/dist/server/handshakeManager.js +472 -0
- package/dist/server/index-server.d.ts +56 -0
- package/dist/server/index-server.js +822 -0
- package/dist/server/registry.d.ts +44 -0
- package/dist/server/registry.js +236 -0
- package/dist/server/sdkServer.d.ts +8 -0
- package/dist/server/sdkServer.js +299 -0
- package/dist/server/shutdownGuard.d.ts +41 -0
- package/dist/server/shutdownGuard.js +52 -0
- package/dist/server/thin-client.d.ts +22 -0
- package/dist/server/thin-client.js +111 -0
- package/dist/server/transport.d.ts +41 -0
- package/dist/server/transport.js +312 -0
- package/dist/server/transportFactory.d.ts +21 -0
- package/dist/server/transportFactory.js +429 -0
- package/dist/services/atomicFs.d.ts +22 -0
- package/dist/services/atomicFs.js +103 -0
- package/dist/services/auditLog.d.ts +38 -0
- package/dist/services/auditLog.js +142 -0
- package/dist/services/autoBackup.d.ts +14 -0
- package/dist/services/autoBackup.js +171 -0
- package/dist/services/autoSplit.d.ts +32 -0
- package/dist/services/autoSplit.js +113 -0
- package/dist/services/backupZip.d.ts +25 -0
- package/dist/services/backupZip.js +110 -0
- package/dist/services/bootstrapGating.d.ts +123 -0
- package/dist/services/bootstrapGating.js +221 -0
- package/dist/services/canonical.d.ts +23 -0
- package/dist/services/canonical.js +65 -0
- package/dist/services/categoryRules.d.ts +7 -0
- package/dist/services/categoryRules.js +37 -0
- package/dist/services/classificationService.d.ts +42 -0
- package/dist/services/classificationService.js +168 -0
- package/dist/services/embeddingService.d.ts +62 -0
- package/dist/services/embeddingService.js +259 -0
- package/dist/services/errors.d.ts +22 -0
- package/dist/services/errors.js +31 -0
- package/dist/services/featureFlags.d.ts +25 -0
- package/dist/services/featureFlags.js +89 -0
- package/dist/services/features.d.ts +13 -0
- package/dist/services/features.js +35 -0
- package/dist/services/handlers/instructions.add.d.ts +1 -0
- package/dist/services/handlers/instructions.add.js +496 -0
- package/dist/services/handlers/instructions.groom.d.ts +1 -0
- package/dist/services/handlers/instructions.groom.js +523 -0
- package/dist/services/handlers/instructions.import.d.ts +1 -0
- package/dist/services/handlers/instructions.import.js +173 -0
- package/dist/services/handlers/instructions.patch.d.ts +1 -0
- package/dist/services/handlers/instructions.patch.js +167 -0
- package/dist/services/handlers/instructions.query.d.ts +163 -0
- package/dist/services/handlers/instructions.query.js +522 -0
- package/dist/services/handlers/instructions.reload.d.ts +1 -0
- package/dist/services/handlers/instructions.reload.js +13 -0
- package/dist/services/handlers/instructions.remove.d.ts +1 -0
- package/dist/services/handlers/instructions.remove.js +118 -0
- package/dist/services/handlers/instructions.shared.d.ts +31 -0
- package/dist/services/handlers/instructions.shared.js +124 -0
- package/dist/services/handlers.activation.d.ts +1 -0
- package/dist/services/handlers.activation.js +203 -0
- package/dist/services/handlers.bootstrap.d.ts +1 -0
- package/dist/services/handlers.bootstrap.js +38 -0
- package/dist/services/handlers.dashboardConfig.d.ts +34 -0
- package/dist/services/handlers.dashboardConfig.js +108 -0
- package/dist/services/handlers.diagnostics.d.ts +1 -0
- package/dist/services/handlers.diagnostics.js +64 -0
- package/dist/services/handlers.feedback.d.ts +15 -0
- package/dist/services/handlers.feedback.js +378 -0
- package/dist/services/handlers.gates.d.ts +1 -0
- package/dist/services/handlers.gates.js +46 -0
- package/dist/services/handlers.graph.d.ts +53 -0
- package/dist/services/handlers.graph.js +231 -0
- package/dist/services/handlers.help.d.ts +1 -0
- package/dist/services/handlers.help.js +119 -0
- package/dist/services/handlers.instructionSchema.d.ts +1 -0
- package/dist/services/handlers.instructionSchema.js +227 -0
- package/dist/services/handlers.instructions.d.ts +8 -0
- package/dist/services/handlers.instructions.js +14 -0
- package/dist/services/handlers.instructionsDiagnostics.d.ts +1 -0
- package/dist/services/handlers.instructionsDiagnostics.js +14 -0
- package/dist/services/handlers.integrity.d.ts +1 -0
- package/dist/services/handlers.integrity.js +35 -0
- package/dist/services/handlers.manifest.d.ts +1 -0
- package/dist/services/handlers.manifest.js +24 -0
- package/dist/services/handlers.messaging.d.ts +12 -0
- package/dist/services/handlers.messaging.js +203 -0
- package/dist/services/handlers.metrics.d.ts +1 -0
- package/dist/services/handlers.metrics.js +43 -0
- package/dist/services/handlers.promote.d.ts +1 -0
- package/dist/services/handlers.promote.js +306 -0
- package/dist/services/handlers.prompt.d.ts +1 -0
- package/dist/services/handlers.prompt.js +7 -0
- package/dist/services/handlers.search.d.ts +69 -0
- package/dist/services/handlers.search.js +645 -0
- package/dist/services/handlers.testPrimitive.d.ts +1 -0
- package/dist/services/handlers.testPrimitive.js +5 -0
- package/dist/services/handlers.trace.d.ts +1 -0
- package/dist/services/handlers.trace.js +31 -0
- package/dist/services/handlers.usage.d.ts +1 -0
- package/dist/services/handlers.usage.js +11 -0
- package/dist/services/hotScore.d.ts +137 -0
- package/dist/services/hotScore.js +244 -0
- package/dist/services/indexContext.d.ts +117 -0
- package/dist/services/indexContext.js +968 -0
- package/dist/services/indexLoader.d.ts +44 -0
- package/dist/services/indexLoader.js +921 -0
- package/dist/services/indexRepository.d.ts +32 -0
- package/dist/services/indexRepository.js +71 -0
- package/dist/services/indexingService.d.ts +1 -0
- package/dist/services/indexingService.js +2 -0
- package/dist/services/instructions.dispatcher.d.ts +1 -0
- package/dist/services/instructions.dispatcher.js +231 -0
- package/dist/services/logPrefix.d.ts +1 -0
- package/dist/services/logPrefix.js +30 -0
- package/dist/services/logger.d.ts +52 -0
- package/dist/services/logger.js +268 -0
- package/dist/services/manifestManager.d.ts +82 -0
- package/dist/services/manifestManager.js +200 -0
- package/dist/services/messaging/agentMailbox.d.ts +60 -0
- package/dist/services/messaging/agentMailbox.js +353 -0
- package/dist/services/messaging/messagingPersistence.d.ts +20 -0
- package/dist/services/messaging/messagingPersistence.js +111 -0
- package/dist/services/messaging/messagingTypes.d.ts +150 -0
- package/dist/services/messaging/messagingTypes.js +66 -0
- package/dist/services/ownershipService.d.ts +1 -0
- package/dist/services/ownershipService.js +38 -0
- package/dist/services/performanceBaseline.d.ts +19 -0
- package/dist/services/performanceBaseline.js +210 -0
- package/dist/services/preflight.d.ts +12 -0
- package/dist/services/preflight.js +79 -0
- package/dist/services/promptReviewService.d.ts +44 -0
- package/dist/services/promptReviewService.js +101 -0
- package/dist/services/responseEnvelope.d.ts +6 -0
- package/dist/services/responseEnvelope.js +25 -0
- package/dist/services/seedBootstrap.d.ts +34 -0
- package/dist/services/seedBootstrap.js +427 -0
- package/dist/services/storage/factory.d.ts +17 -0
- package/dist/services/storage/factory.js +35 -0
- package/dist/services/storage/hashUtils.d.ts +11 -0
- package/dist/services/storage/hashUtils.js +35 -0
- package/dist/services/storage/index.d.ts +12 -0
- package/dist/services/storage/index.js +18 -0
- package/dist/services/storage/jsonFileStore.d.ts +32 -0
- package/dist/services/storage/jsonFileStore.js +241 -0
- package/dist/services/storage/migrationEngine.d.ts +35 -0
- package/dist/services/storage/migrationEngine.js +93 -0
- package/dist/services/storage/sqliteMessageStore.d.ts +53 -0
- package/dist/services/storage/sqliteMessageStore.js +146 -0
- package/dist/services/storage/sqliteSchema.d.ts +12 -0
- package/dist/services/storage/sqliteSchema.js +122 -0
- package/dist/services/storage/sqliteStore.d.ts +41 -0
- package/dist/services/storage/sqliteStore.js +339 -0
- package/dist/services/storage/sqliteUsageStore.d.ts +35 -0
- package/dist/services/storage/sqliteUsageStore.js +94 -0
- package/dist/services/storage/types.d.ts +171 -0
- package/dist/services/storage/types.js +12 -0
- package/dist/services/toolHandlers.d.ts +23 -0
- package/dist/services/toolHandlers.js +50 -0
- package/dist/services/toolRegistry.d.ts +20 -0
- package/dist/services/toolRegistry.js +490 -0
- package/dist/services/toolRegistry.zod.d.ts +10 -0
- package/dist/services/toolRegistry.zod.js +323 -0
- package/dist/services/tracing.d.ts +26 -0
- package/dist/services/tracing.js +260 -0
- package/dist/services/usageBuckets.d.ts +161 -0
- package/dist/services/usageBuckets.js +364 -0
- package/dist/services/validationService.d.ts +38 -0
- package/dist/services/validationService.js +125 -0
- package/dist/utils/BufferRing.d.ts +203 -0
- package/dist/utils/BufferRing.js +551 -0
- package/dist/utils/BufferRingExamples.d.ts +55 -0
- package/dist/utils/BufferRingExamples.js +188 -0
- package/dist/utils/envUtils.d.ts +42 -0
- package/dist/utils/envUtils.js +80 -0
- package/dist/utils/memoryMonitor.d.ts +83 -0
- package/dist/utils/memoryMonitor.js +275 -0
- package/dist/versioning/schemaVersion.d.ts +6 -0
- package/dist/versioning/schemaVersion.js +93 -0
- package/package.json +134 -0
- package/schemas/README.md +13 -0
- package/schemas/feedback-entry.schema.json +27 -0
- package/schemas/graph-export-v2.schema.json +60 -0
- package/schemas/index-server.code-schema.json +38477 -0
- package/schemas/instruction.schema.json +262 -0
- package/schemas/json-schema/SessionPersistence-persisted-admin-session.schema.json +54 -0
- package/schemas/json-schema/SessionPersistence-persisted-session-history-entry.schema.json +51 -0
- package/schemas/json-schema/SessionPersistence-persisted-web-socket-connection.schema.json +54 -0
- package/schemas/json-schema/SessionPersistence-session-persistence-config.schema.json +110 -0
- package/schemas/json-schema/SessionPersistence-session-persistence-data.schema.json +229 -0
- package/schemas/json-schema/SessionPersistence-session-persistence-manifest.schema.json +109 -0
- package/schemas/json-schema/SessionPersistence-session-persistence-metadata.schema.json +55 -0
- package/schemas/json-schema/instruction-audience-scope.schema.json +14 -0
- package/schemas/json-schema/instruction-content-type.schema.json +17 -0
- package/schemas/json-schema/instruction-instruction-entry.schema.json +206 -0
- package/schemas/json-schema/instruction-requirement-level.schema.json +16 -0
- package/schemas/manifest.json +78 -0
- package/schemas/manifest.schema.json +33 -0
- package/schemas/usage-batch.schema.json +16 -0
- package/schemas/usage-buckets.schema.json +30 -0
- package/schemas/usage-event.schema.json +17 -0
- package/scripts/copy-dashboard-assets.mjs +170 -0
- 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;
|