@cisco_open/linting-orchestrator 1.0.0-rc.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +5 -0
  3. package/README.md +43 -0
  4. package/build/cli/api-client.d.ts +170 -0
  5. package/build/cli/api-client.d.ts.map +1 -0
  6. package/build/cli/api-client.js +284 -0
  7. package/build/cli/api-client.js.map +1 -0
  8. package/build/cli/commands/agents.d.ts +7 -0
  9. package/build/cli/commands/agents.d.ts.map +1 -0
  10. package/build/cli/commands/agents.js +694 -0
  11. package/build/cli/commands/agents.js.map +1 -0
  12. package/build/cli/commands/completion.d.ts +9 -0
  13. package/build/cli/commands/completion.d.ts.map +1 -0
  14. package/build/cli/commands/completion.js +177 -0
  15. package/build/cli/commands/completion.js.map +1 -0
  16. package/build/cli/commands/config.d.ts +10 -0
  17. package/build/cli/commands/config.d.ts.map +1 -0
  18. package/build/cli/commands/config.js +284 -0
  19. package/build/cli/commands/config.js.map +1 -0
  20. package/build/cli/commands/health.d.ts +11 -0
  21. package/build/cli/commands/health.d.ts.map +1 -0
  22. package/build/cli/commands/health.js +38 -0
  23. package/build/cli/commands/health.js.map +1 -0
  24. package/build/cli/commands/help.d.ts +6 -0
  25. package/build/cli/commands/help.d.ts.map +1 -0
  26. package/build/cli/commands/help.js +20 -0
  27. package/build/cli/commands/help.js.map +1 -0
  28. package/build/cli/commands/history.d.ts +11 -0
  29. package/build/cli/commands/history.d.ts.map +1 -0
  30. package/build/cli/commands/history.js +50 -0
  31. package/build/cli/commands/history.js.map +1 -0
  32. package/build/cli/commands/jobs.d.ts +12 -0
  33. package/build/cli/commands/jobs.d.ts.map +1 -0
  34. package/build/cli/commands/jobs.js +84 -0
  35. package/build/cli/commands/jobs.js.map +1 -0
  36. package/build/cli/commands/lint.d.ts +15 -0
  37. package/build/cli/commands/lint.d.ts.map +1 -0
  38. package/build/cli/commands/lint.js +384 -0
  39. package/build/cli/commands/lint.js.map +1 -0
  40. package/build/cli/commands/ps.d.ts +8 -0
  41. package/build/cli/commands/ps.d.ts.map +1 -0
  42. package/build/cli/commands/ps.js +74 -0
  43. package/build/cli/commands/ps.js.map +1 -0
  44. package/build/cli/commands/reproduce.d.ts +9 -0
  45. package/build/cli/commands/reproduce.d.ts.map +1 -0
  46. package/build/cli/commands/reproduce.js +31 -0
  47. package/build/cli/commands/reproduce.js.map +1 -0
  48. package/build/cli/commands/reset.d.ts +5 -0
  49. package/build/cli/commands/reset.d.ts.map +1 -0
  50. package/build/cli/commands/reset.js +13 -0
  51. package/build/cli/commands/reset.js.map +1 -0
  52. package/build/cli/commands/results.d.ts +13 -0
  53. package/build/cli/commands/results.d.ts.map +1 -0
  54. package/build/cli/commands/results.js +129 -0
  55. package/build/cli/commands/results.js.map +1 -0
  56. package/build/cli/commands/rulesets/check.d.ts +12 -0
  57. package/build/cli/commands/rulesets/check.d.ts.map +1 -0
  58. package/build/cli/commands/rulesets/check.js +226 -0
  59. package/build/cli/commands/rulesets/check.js.map +1 -0
  60. package/build/cli/commands/rulesets/index.d.ts +5 -0
  61. package/build/cli/commands/rulesets/index.d.ts.map +1 -0
  62. package/build/cli/commands/rulesets/index.js +6 -0
  63. package/build/cli/commands/rulesets/index.js.map +1 -0
  64. package/build/cli/commands/rulesets/view.d.ts +16 -0
  65. package/build/cli/commands/rulesets/view.d.ts.map +1 -0
  66. package/build/cli/commands/rulesets/view.js +100 -0
  67. package/build/cli/commands/rulesets/view.js.map +1 -0
  68. package/build/cli/commands/start.d.ts +16 -0
  69. package/build/cli/commands/start.d.ts.map +1 -0
  70. package/build/cli/commands/start.js +167 -0
  71. package/build/cli/commands/start.js.map +1 -0
  72. package/build/cli/commands/status.d.ts +9 -0
  73. package/build/cli/commands/status.d.ts.map +1 -0
  74. package/build/cli/commands/status.js +46 -0
  75. package/build/cli/commands/status.js.map +1 -0
  76. package/build/cli/commands/stop.d.ts +11 -0
  77. package/build/cli/commands/stop.d.ts.map +1 -0
  78. package/build/cli/commands/stop.js +78 -0
  79. package/build/cli/commands/stop.js.map +1 -0
  80. package/build/cli/config-manager.d.ts +134 -0
  81. package/build/cli/config-manager.d.ts.map +1 -0
  82. package/build/cli/config-manager.js +288 -0
  83. package/build/cli/config-manager.js.map +1 -0
  84. package/build/cli/formatters.d.ts +62 -0
  85. package/build/cli/formatters.d.ts.map +1 -0
  86. package/build/cli/formatters.js +715 -0
  87. package/build/cli/formatters.js.map +1 -0
  88. package/build/cli/history-manager.d.ts +97 -0
  89. package/build/cli/history-manager.d.ts.map +1 -0
  90. package/build/cli/history-manager.js +201 -0
  91. package/build/cli/history-manager.js.map +1 -0
  92. package/build/cli/index.d.ts +16 -0
  93. package/build/cli/index.d.ts.map +1 -0
  94. package/build/cli/index.js +335 -0
  95. package/build/cli/index.js.map +1 -0
  96. package/build/cli/list-rulesets.d.ts +15 -0
  97. package/build/cli/list-rulesets.d.ts.map +1 -0
  98. package/build/cli/list-rulesets.js +193 -0
  99. package/build/cli/list-rulesets.js.map +1 -0
  100. package/build/cli/utils/connection-error.d.ts +9 -0
  101. package/build/cli/utils/connection-error.d.ts.map +1 -0
  102. package/build/cli/utils/connection-error.js +30 -0
  103. package/build/cli/utils/connection-error.js.map +1 -0
  104. package/build/cli/utils/embedded-server.d.ts +21 -0
  105. package/build/cli/utils/embedded-server.d.ts.map +1 -0
  106. package/build/cli/utils/embedded-server.js +61 -0
  107. package/build/cli/utils/embedded-server.js.map +1 -0
  108. package/build/cli/utils/mode-validator.d.ts +13 -0
  109. package/build/cli/utils/mode-validator.d.ts.map +1 -0
  110. package/build/cli/utils/mode-validator.js +31 -0
  111. package/build/cli/utils/mode-validator.js.map +1 -0
  112. package/build/cli/utils/port-checker.d.ts +20 -0
  113. package/build/cli/utils/port-checker.d.ts.map +1 -0
  114. package/build/cli/utils/port-checker.js +49 -0
  115. package/build/cli/utils/port-checker.js.map +1 -0
  116. package/build/config.d.ts +57 -0
  117. package/build/config.d.ts.map +1 -0
  118. package/build/config.js +527 -0
  119. package/build/config.js.map +1 -0
  120. package/build/document-accessor.d.ts +79 -0
  121. package/build/document-accessor.d.ts.map +1 -0
  122. package/build/document-accessor.js +148 -0
  123. package/build/document-accessor.js.map +1 -0
  124. package/build/formatters/reproduce-markdown.d.ts +14 -0
  125. package/build/formatters/reproduce-markdown.d.ts.map +1 -0
  126. package/build/formatters/reproduce-markdown.js +182 -0
  127. package/build/formatters/reproduce-markdown.js.map +1 -0
  128. package/build/formatters/sarif-builder.d.ts +86 -0
  129. package/build/formatters/sarif-builder.d.ts.map +1 -0
  130. package/build/formatters/sarif-builder.js +276 -0
  131. package/build/formatters/sarif-builder.js.map +1 -0
  132. package/build/index.d.ts +3 -0
  133. package/build/index.d.ts.map +1 -0
  134. package/build/index.js +174 -0
  135. package/build/index.js.map +1 -0
  136. package/build/logger.d.ts +38 -0
  137. package/build/logger.d.ts.map +1 -0
  138. package/build/logger.js +105 -0
  139. package/build/logger.js.map +1 -0
  140. package/build/mock-server.d.ts +2 -0
  141. package/build/mock-server.d.ts.map +1 -0
  142. package/build/mock-server.js +290 -0
  143. package/build/mock-server.js.map +1 -0
  144. package/build/orchestrator.d.ts +149 -0
  145. package/build/orchestrator.d.ts.map +1 -0
  146. package/build/orchestrator.js +874 -0
  147. package/build/orchestrator.js.map +1 -0
  148. package/build/ruleset-loader.d.ts +79 -0
  149. package/build/ruleset-loader.d.ts.map +1 -0
  150. package/build/ruleset-loader.js +514 -0
  151. package/build/ruleset-loader.js.map +1 -0
  152. package/build/schemas.d.ts +2568 -0
  153. package/build/schemas.d.ts.map +1 -0
  154. package/build/schemas.js +674 -0
  155. package/build/schemas.js.map +1 -0
  156. package/build/server.d.ts +39 -0
  157. package/build/server.d.ts.map +1 -0
  158. package/build/server.js +834 -0
  159. package/build/server.js.map +1 -0
  160. package/build/storage/memory-storage.d.ts +190 -0
  161. package/build/storage/memory-storage.d.ts.map +1 -0
  162. package/build/storage/memory-storage.js +629 -0
  163. package/build/storage/memory-storage.js.map +1 -0
  164. package/build/storage/redis-storage.d.ts +134 -0
  165. package/build/storage/redis-storage.d.ts.map +1 -0
  166. package/build/storage/redis-storage.js +236 -0
  167. package/build/storage/redis-storage.js.map +1 -0
  168. package/build/storage/storage-adapter.d.ts +189 -0
  169. package/build/storage/storage-adapter.d.ts.map +1 -0
  170. package/build/storage/storage-adapter.js +36 -0
  171. package/build/storage/storage-adapter.js.map +1 -0
  172. package/build/types.d.ts +981 -0
  173. package/build/types.d.ts.map +1 -0
  174. package/build/types.js +5 -0
  175. package/build/types.js.map +1 -0
  176. package/build/utils/version.d.ts +40 -0
  177. package/build/utils/version.d.ts.map +1 -0
  178. package/build/utils/version.js +94 -0
  179. package/build/utils/version.js.map +1 -0
  180. package/build/validation.d.ts +95 -0
  181. package/build/validation.d.ts.map +1 -0
  182. package/build/validation.js +150 -0
  183. package/build/validation.js.map +1 -0
  184. package/build/worker-pool.d.ts +137 -0
  185. package/build/worker-pool.d.ts.map +1 -0
  186. package/build/worker-pool.js +549 -0
  187. package/build/worker-pool.js.map +1 -0
  188. package/build/worker.d.ts +2 -0
  189. package/build/worker.d.ts.map +1 -0
  190. package/build/worker.js +427 -0
  191. package/build/worker.js.map +1 -0
  192. package/package.json +110 -0
  193. package/rulesets/CHANGELOG.md +25 -0
  194. package/rulesets/config/rulesets.yaml +96 -0
  195. package/rulesets/sources/README.md +47 -0
  196. package/rulesets/sources/example/oas-recommended/v1.0.0/ruleset.yaml +6 -0
  197. package/rulesets/sources/example/oas-recommended/v2.0.0/ruleset.yaml +14 -0
@@ -0,0 +1,834 @@
1
+ // Copyright 2026 Cisco Systems, Inc. and its affiliates
2
+ //
3
+ // SPDX-License-Identifier: Apache-2.0
4
+ /**
5
+ * Orchestrator server - Exportable server module for all deployment modes
6
+ *
7
+ * This module provides the core server functionality that can be:
8
+ * - Imported and run in-process (embedded mode - embedded in CLI)
9
+ * - Run as standalone process (standalone mode - dedicated server)
10
+ * - Integrated with MCP (companion mode - MCP-managed)
11
+ *
12
+ * @module server
13
+ */
14
+ import Fastify from 'fastify';
15
+ import swagger from '@fastify/swagger';
16
+ import path from 'path';
17
+ import crypto from 'crypto';
18
+ import { API_VERSION, SPECTRAL_CORE_VERSION, SPECTRAL_RULESETS_VERSION, SPECTRAL_CLI_VERSION } from './utils/version.js';
19
+ import { loadConfig, resolveResolverPath } from './config.js';
20
+ import { Orchestrator, CapacityExceededError } from './orchestrator.js';
21
+ import { WorkerPoolManager } from './worker-pool.js';
22
+ import { RulesetLoader } from './ruleset-loader.js';
23
+ import { LocalDocumentStore, PassThroughDocumentStore, } from '@cisco_open/linting-document-store';
24
+ import { MemoryLintStorage } from './storage/memory-storage.js';
25
+ import { SarifBuilder } from './formatters/sarif-builder.js';
26
+ import { generateReproductionMarkdown } from './formatters/reproduce-markdown.js';
27
+ import { lintJobRequestSchema, jobIdParamSchema, documentIdParamSchema, errorHandler, createErrorResponse, sanitizeDocumentId, sanitizeRulesetName } from './validation.js';
28
+ import { sharedSchemas, DocumentListQuerySchema, JobListQuerySchema, RulesetNameParamSchema, RulesetVersionQuerySchema, } from './schemas.js';
29
+ /**
30
+ * Start orchestrator server with given configuration
31
+ *
32
+ * This function is used by:
33
+ * - src/index.ts (standalone server entrypoint)
34
+ * - src/cli/commands/start.ts (embedded server)
35
+ * - Tests (integration testing)
36
+ *
37
+ * @param customConfig Optional configuration to override defaults
38
+ * @returns ServerInstance with shutdown capability
39
+ */
40
+ export async function startServer(customConfig) {
41
+ console.log('🚀 Starting the Orchestrator service...\n');
42
+ // 1. Load configuration
43
+ const config = customConfig
44
+ ? { ...await loadConfig(), ...customConfig }
45
+ : await loadConfig();
46
+ console.log('✅ Configuration loaded');
47
+ console.log(` Mode: ${config.mode || 'standalone'}`);
48
+ console.log(` Port: ${config.server?.port || 3003}`);
49
+ console.log(` Directory: ${config.documentStore.baseDir}\n`);
50
+ // 2. Initialize storage
51
+ const storage = new MemoryLintStorage();
52
+ await storage.initialize({});
53
+ console.log('✅ Storage initialized\n');
54
+ // 3. Initialize ruleset loader
55
+ const rulesetsConfigPath = path.join(config.rulesets.directory, 'config', 'rulesets.yaml');
56
+ console.log(` Loading rulesets from: ${rulesetsConfigPath}`);
57
+ const rulesetLoader = new RulesetLoader({
58
+ configPath: rulesetsConfigPath,
59
+ sourcesBasePath: path.join(config.rulesets.directory, 'sources')
60
+ });
61
+ await rulesetLoader.initialize();
62
+ console.log('✅ Ruleset loader initialized\n');
63
+ // 4. Initialize document store (based on config type)
64
+ let documentStore;
65
+ if (config.documentStore.type === 'passthrough') {
66
+ // PassThrough mode - read from external document store (e.g., MCP)
67
+ documentStore = new PassThroughDocumentStore({
68
+ uploadsDir: config.documentStore.baseDir,
69
+ httpFallbackUrl: config.documentStore.fallbackHttp
70
+ });
71
+ console.log(' Using PassThroughDocumentStore (companion mode)');
72
+ }
73
+ else {
74
+ // Standalone mode - use local storage with cache
75
+ documentStore = new LocalDocumentStore({
76
+ uploadsDir: config.documentStore.baseDir || './uploads',
77
+ quotaGB: 10,
78
+ cacheMaxSize: 100,
79
+ cacheTTLHours: 1
80
+ });
81
+ console.log(' Using LocalDocumentStore (standalone/embedded mode)');
82
+ }
83
+ await documentStore.initialize();
84
+ console.log('✅ Document store initialized\n');
85
+ // 5. Initialize worker pool
86
+ const resolverPath = resolveResolverPath(config.resolver);
87
+ const workerPool = new WorkerPoolManager(config.workerPool, rulesetLoader, documentStore, resolverPath);
88
+ await workerPool.initialize();
89
+ console.log('✅ Worker pool initialized');
90
+ if (resolverPath) {
91
+ console.log(` Resolver: ${config.resolver} (${resolverPath})`);
92
+ }
93
+ console.log('');
94
+ // 6. Initialize orchestrator
95
+ const maxIssuesPerJob = process.env.MAX_ISSUES_PER_JOB
96
+ ? parseInt(process.env.MAX_ISSUES_PER_JOB, 10)
97
+ : 100000;
98
+ const orchestrator = new Orchestrator(workerPool, storage, rulesetLoader, documentStore, {
99
+ workerPool: config.workerPool,
100
+ reportService: config.reportService,
101
+ maxIssuesPerJob
102
+ });
103
+ await orchestrator.initialize();
104
+ console.log('✅ Orchestrator initialized\n');
105
+ // 7. Initialize SARIF builder
106
+ const sarifBuilder = new SarifBuilder();
107
+ console.log('✅ SARIF builder initialized\n');
108
+ // 8. Generate runtime session ID (for client history management)
109
+ const runtimeSessionId = crypto.randomUUID();
110
+ console.log(`🔑 Runtime Session ID: ${runtimeSessionId}\n`);
111
+ // 8. Create HTTP server
112
+ const maxDocumentBytes = (config.server?.maxDocumentSizeMB || 20) * 1024 * 1024;
113
+ const fastify = Fastify({
114
+ logger: config.logging?.level === 'debug',
115
+ bodyLimit: maxDocumentBytes,
116
+ ajv: {
117
+ customOptions: {
118
+ // Register 'example' as a known keyword so AJV strict mode doesn't reject it.
119
+ // This allows the same schemas to feed both AJV validation and @fastify/swagger
120
+ // OpenAPI generation without duplication.
121
+ keywords: ['example'],
122
+ },
123
+ },
124
+ });
125
+ // Register @fastify/swagger to auto-generate OpenAPI spec from route schemas
126
+ await fastify.register(swagger, {
127
+ openapi: {
128
+ openapi: '3.0.3',
129
+ info: {
130
+ title: 'Linting Orchestrator API',
131
+ version: API_VERSION,
132
+ description: 'The linting orchestrator uses Spectral and custom rule engines.\n' +
133
+ 'It provides a REST API for uploading documents, submitting lint jobs, and retrieving results.\n\n' +
134
+ '**Deployment Modes:**\n' +
135
+ '- **Light Mode** (Embedded): CLI embeds server, zero-config, perfect for CI/CD\n' +
136
+ '- **Standalone Mode**: Independent server process, long-running, multi-user\n' +
137
+ '- **Companion Mode**: Runs alongside the MCP OpenAPI Analyzer for integrated document lifecycle',
138
+ contact: { name: 'Cisco DevNet' },
139
+ license: { name: 'Apache-2.0', url: 'https://opensource.org/licenses/Apache-2.0' },
140
+ },
141
+ servers: [
142
+ { url: `http://localhost:${config.server?.port || 3003}`, description: 'Local development server (default port)' },
143
+ ],
144
+ tags: [
145
+ { name: 'Health', description: 'Server health and status' },
146
+ { name: 'Documents', description: 'Upload and manage documents' },
147
+ { name: 'Linting', description: 'Submit lint jobs and retrieve results' },
148
+ { name: 'Jobs', description: 'List and query lint jobs' },
149
+ { name: 'Reports', description: 'Generate reports from lint results' },
150
+ { name: 'Rulesets', description: 'Browse available rulesets and rules' },
151
+ { name: 'Cache', description: 'Cache management' },
152
+ { name: 'Statistics', description: 'Server and storage statistics' },
153
+ ],
154
+ },
155
+ // Use $id as the schema component name instead of the default "def-N" naming
156
+ refResolver: {
157
+ buildLocalReference(json, _baseUri, _fragment, i) {
158
+ if (json.$id) {
159
+ return json.$id;
160
+ }
161
+ if (json.title) {
162
+ return json.title;
163
+ }
164
+ return `def-${i}`;
165
+ },
166
+ },
167
+ });
168
+ // Register shared JSON Schemas so routes can $ref them
169
+ for (const schema of sharedSchemas) {
170
+ fastify.addSchema(schema);
171
+ }
172
+ // Add runtime session ID header to all responses
173
+ fastify.addHook('onSend', async (_request, reply) => {
174
+ reply.header('X-Spectify-Session-Id', runtimeSessionId);
175
+ reply.header('X-Spectify-Version', API_VERSION);
176
+ });
177
+ // Set global error handler with document size context
178
+ fastify.setErrorHandler((error, request, reply) => {
179
+ // Handle body too large errors with clear message
180
+ if (error.code === 'FST_ERR_CTP_BODY_TOO_LARGE') {
181
+ const sizeLimitMB = (config.server?.maxDocumentSizeMB || 20);
182
+ reply.code(413).send({
183
+ error: 'Payload Too Large',
184
+ message: `Document exceeds maximum size of ${sizeLimitMB}MB. Configure server.maxDocumentSizeMB in config.yaml to increase.`,
185
+ statusCode: 413
186
+ });
187
+ return;
188
+ }
189
+ // Delegate to standard error handler
190
+ errorHandler(error, request, reply);
191
+ });
192
+ // Health check endpoint
193
+ const serverStartTime = new Date();
194
+ fastify.get('/health', {
195
+ schema: {
196
+ description: 'Returns server health status, uptime, configuration, and orchestrator statistics.',
197
+ tags: ['Health'],
198
+ summary: 'Health check',
199
+ operationId: 'getHealth',
200
+ response: {
201
+ 200: { $ref: 'HealthResponse#', description: 'Server is healthy' },
202
+ 500: { $ref: 'ErrorResponse#', description: 'Internal server error' },
203
+ },
204
+ },
205
+ }, async () => {
206
+ const stats = orchestrator.getStats();
207
+ const uptimeSeconds = Math.floor((Date.now() - serverStartTime.getTime()) / 1000);
208
+ const baseDir = config.documentStore?.baseDir || './uploads';
209
+ const health = {
210
+ status: 'ok',
211
+ version: API_VERSION,
212
+ timestamp: new Date().toISOString(),
213
+ uptime: uptimeSeconds,
214
+ mode: config.mode || 'standalone',
215
+ runtime: {
216
+ nodeVersion: process.version,
217
+ spectralCore: SPECTRAL_CORE_VERSION,
218
+ spectralRulesets: SPECTRAL_RULESETS_VERSION,
219
+ spectralCli: SPECTRAL_CLI_VERSION,
220
+ resolver: config.resolver || null,
221
+ },
222
+ server: {
223
+ port: config.server?.port || 3003,
224
+ host: config.server?.host || '0.0.0.0',
225
+ startedAt: serverStartTime.toISOString(),
226
+ },
227
+ documentStore: {
228
+ type: config.documentStore?.type || 'local',
229
+ baseDir: baseDir,
230
+ fullPath: path.resolve(baseDir),
231
+ },
232
+ stats
233
+ };
234
+ // Add Report Service status if configured
235
+ if (orchestrator.hasReportClient()) {
236
+ const reportStatus = await orchestrator.getReportClientStatus();
237
+ health.reportService = reportStatus;
238
+ }
239
+ return health;
240
+ });
241
+ // Upload OpenAPI document (standalone/embedded mode)
242
+ fastify.post('/documents', {
243
+ schema: {
244
+ description: 'Upload a document for linting. Returns a document ID that can be used to submit lint jobs.',
245
+ tags: ['Documents'],
246
+ summary: 'Upload document',
247
+ operationId: 'uploadDocument',
248
+ body: { $ref: 'DocumentUploadRequest#' },
249
+ response: {
250
+ 201: { $ref: 'DocumentUploadResponse#', description: 'Document uploaded successfully' },
251
+ 400: { $ref: 'ErrorResponse#', description: 'Bad request' },
252
+ 413: { $ref: 'ErrorResponse#', description: 'Document exceeds maximum upload size limit' },
253
+ 500: { $ref: 'ErrorResponse#', description: 'Internal server error' },
254
+ },
255
+ },
256
+ }, async (request, reply) => {
257
+ const { content, format = 'json', metadata } = request.body;
258
+ if (!content || typeof content !== 'string') {
259
+ return reply.code(400).send(createErrorResponse('Bad Request', 'Document content is required'));
260
+ }
261
+ try {
262
+ const result = await documentStore.storeDocument(content, format, metadata);
263
+ return reply.code(201).send({
264
+ documentId: result.documentId,
265
+ version: result.version,
266
+ message: 'Document uploaded successfully'
267
+ });
268
+ }
269
+ catch (error) {
270
+ console.error('Document upload failed:', error);
271
+ return reply.code(500).send(createErrorResponse('Internal Server Error', 'Failed to store document'));
272
+ }
273
+ });
274
+ // Get document by ID
275
+ fastify.get('/documents/:documentId', {
276
+ schema: {
277
+ description: 'Retrieve an uploaded document by its identifier.',
278
+ tags: ['Documents'],
279
+ summary: 'Get document by ID',
280
+ operationId: 'getDocument',
281
+ params: documentIdParamSchema,
282
+ response: {
283
+ 200: { $ref: 'DocumentResponse#', description: 'Document found and returned' },
284
+ 404: { $ref: 'ErrorResponse#', description: 'Resource not found' },
285
+ },
286
+ },
287
+ }, async (request, reply) => {
288
+ const { documentId } = request.params;
289
+ const document = await documentStore.getDocument(sanitizeDocumentId(documentId));
290
+ if (!document) {
291
+ return reply.code(404).send(createErrorResponse('Not Found', `Document not found: ${documentId}`));
292
+ }
293
+ return document;
294
+ });
295
+ // List/search documents
296
+ fastify.get('/documents', {
297
+ schema: {
298
+ description: 'List uploaded documents with optional filtering, sorting, and search. When `search` is provided, performs a text search; otherwise returns a paginated list.',
299
+ tags: ['Documents'],
300
+ summary: 'List or search documents',
301
+ operationId: 'listDocuments',
302
+ querystring: DocumentListQuerySchema,
303
+ response: {
304
+ 200: { $ref: 'DocumentListResponse#', description: 'List of documents matching the query' },
305
+ 500: { $ref: 'ErrorResponse#', description: 'Internal server error' },
306
+ },
307
+ },
308
+ }, async (request) => {
309
+ const { limit, offset, sortBy, sortOrder, search, organization, tags, format } = request.query;
310
+ // If search query present, use searchDocuments; otherwise use listDocuments
311
+ const options = {
312
+ limit: limit ? parseInt(limit, 10) : 50,
313
+ offset: offset ? parseInt(offset, 10) : 0,
314
+ sortBy: sortBy || 'uploadedAt',
315
+ sortOrder: sortOrder || 'desc'
316
+ };
317
+ if (search) {
318
+ // Search mode - combine search with optional filters
319
+ const searchOptions = {
320
+ ...options,
321
+ ...(organization && { organization }),
322
+ ...(tags && { tags: tags.split(',') }),
323
+ ...(format && { format })
324
+ };
325
+ const documents = await documentStore.searchDocuments(search, searchOptions);
326
+ return documents;
327
+ }
328
+ else {
329
+ // List mode - optional filters only
330
+ const listOptions = {
331
+ ...options,
332
+ ...(organization && { organization }),
333
+ ...(tags && { tags: tags.split(',') }),
334
+ ...(format && { format })
335
+ };
336
+ const documents = await documentStore.listDocuments(listOptions);
337
+ return documents;
338
+ }
339
+ });
340
+ // Submit lint job
341
+ fastify.post('/lint', {
342
+ schema: {
343
+ description: 'Submit a document for linting against a specified ruleset. Returns a job ID for tracking progress. The job runs asynchronously.',
344
+ tags: ['Linting'],
345
+ summary: 'Submit lint job',
346
+ operationId: 'submitLintJob',
347
+ body: lintJobRequestSchema,
348
+ response: {
349
+ 202: { $ref: 'LintJobSubmitResponse#', description: 'Job submitted and queued for processing' },
350
+ 404: { $ref: 'ErrorResponse#', description: 'Resource not found' },
351
+ 429: { $ref: 'CapacityExceededResponse#', description: 'Too many requests — server at capacity' },
352
+ 500: { $ref: 'ErrorResponse#', description: 'Internal server error' },
353
+ 503: { $ref: 'ErrorResponse#', description: 'Service unavailable (server starting up)' },
354
+ },
355
+ },
356
+ }, async (request, reply) => {
357
+ const jobRequest = request.body;
358
+ // Sanitize inputs
359
+ const sanitizedRequest = {
360
+ ...jobRequest,
361
+ documentId: sanitizeDocumentId(jobRequest.documentId),
362
+ rulesetName: sanitizeRulesetName(jobRequest.rulesetName),
363
+ ruleOverrides: jobRequest.ruleOverrides
364
+ };
365
+ try {
366
+ const jobId = await orchestrator.submitJob(sanitizedRequest);
367
+ return reply.code(202).send({
368
+ jobId,
369
+ status: 'queued',
370
+ message: 'Job submitted successfully'
371
+ });
372
+ }
373
+ catch (error) {
374
+ // Backpressure: reject with 429 when at capacity
375
+ if (error instanceof CapacityExceededError) {
376
+ const retryAfter = Math.ceil(error.activeJobs / 10); // ~1s per 10 active jobs
377
+ reply.header('Retry-After', String(retryAfter));
378
+ return reply.code(429).send({
379
+ error: 'Too Many Requests',
380
+ message: error.message,
381
+ activeJobs: error.activeJobs,
382
+ maxJobs: error.maxJobs,
383
+ retryAfter
384
+ });
385
+ }
386
+ // Document not found
387
+ if (error instanceof Error && error.message.startsWith('Document not found')) {
388
+ return reply.code(404).send(createErrorResponse('Not Found', error.message));
389
+ }
390
+ // Orchestrator not initialized
391
+ if (error instanceof Error && error.message === 'Orchestrator not initialized') {
392
+ return reply.code(503).send(createErrorResponse('Service Unavailable', 'Server is starting up, please retry shortly'));
393
+ }
394
+ // Unknown error
395
+ console.error('Job submission failed:', error);
396
+ return reply.code(500).send(createErrorResponse('Internal Server Error', 'Failed to submit lint job'));
397
+ }
398
+ });
399
+ // Get job status
400
+ fastify.get('/lint/:jobId', {
401
+ schema: {
402
+ description: 'Retrieve the current status and progress of a lint job.',
403
+ tags: ['Linting'],
404
+ summary: 'Get job status',
405
+ operationId: 'getJobStatus',
406
+ params: jobIdParamSchema,
407
+ response: {
408
+ 200: { $ref: 'LintJobStatus#', description: 'Current job status and progress' },
409
+ 404: { $ref: 'ErrorResponse#', description: 'Resource not found' },
410
+ },
411
+ },
412
+ }, async (request, reply) => {
413
+ const { jobId } = request.params;
414
+ const job = await orchestrator.getJobStatus(jobId);
415
+ if (!job) {
416
+ return reply.code(404).send(createErrorResponse('Not Found', `Job not found: ${jobId}`));
417
+ }
418
+ return {
419
+ jobId: job.jobId,
420
+ documentId: job.documentId,
421
+ rulesetName: job.rulesetName,
422
+ rulesetVersion: job.rulesetVersion,
423
+ status: job.status,
424
+ progress: job.progress,
425
+ startTime: job.startTime,
426
+ endTime: job.endTime
427
+ };
428
+ });
429
+ // Get job results (with optional pagination and filtering)
430
+ fastify.get('/lint/:jobId/results', {
431
+ schema: {
432
+ description: 'Retrieve the results of a completed lint job. Supports optional pagination and filtering via query parameters. When no query params are provided, returns the full result (backward compatible).',
433
+ tags: ['Linting'],
434
+ summary: 'Get job results',
435
+ operationId: 'getJobResults',
436
+ params: jobIdParamSchema,
437
+ querystring: {
438
+ type: 'object',
439
+ properties: {
440
+ offset: { type: 'string', description: 'Skip first N issues (default: 0)' },
441
+ limit: { type: 'string', description: 'Max issues to return (default: all)' },
442
+ severity: { type: 'string', description: 'Filter by severity: 0=error, 1=warn, 2=info, 3=hint' },
443
+ rule: { type: 'string', description: 'Filter by rule ID (exact match)' },
444
+ pathPrefix: { type: 'string', description: 'Filter issues whose path starts with this prefix (dot-separated)' },
445
+ },
446
+ },
447
+ response: {
448
+ 200: { description: 'Job results (full or paginated)' },
449
+ 404: { $ref: 'ErrorResponse#', description: 'Resource not found' },
450
+ },
451
+ },
452
+ }, async (request, reply) => {
453
+ const { jobId } = request.params;
454
+ const { offset, limit, severity, rule, pathPrefix } = request.query;
455
+ const hasPaginationOrFilter = offset !== undefined || limit !== undefined ||
456
+ severity !== undefined || rule !== undefined || pathPrefix !== undefined;
457
+ if (hasPaginationOrFilter && storage.queryJobResults) {
458
+ // Use paginated query
459
+ const options = {};
460
+ if (offset !== undefined)
461
+ options.offset = parseInt(offset, 10);
462
+ if (limit !== undefined)
463
+ options.limit = parseInt(limit, 10);
464
+ if (severity !== undefined)
465
+ options.severity = parseInt(severity, 10);
466
+ if (rule !== undefined)
467
+ options.rule = rule;
468
+ if (pathPrefix !== undefined)
469
+ options.pathPrefix = pathPrefix;
470
+ const result = await storage.queryJobResults(jobId, options);
471
+ if (!result) {
472
+ return reply.code(404).send(createErrorResponse('Not Found', `Job not found or not yet completed: ${jobId}`));
473
+ }
474
+ return result;
475
+ }
476
+ // Backward compatible: return full result
477
+ const result = await orchestrator.getJobResult(jobId);
478
+ if (!result) {
479
+ return reply.code(404).send(createErrorResponse('Not Found', `Job not found or not yet completed: ${jobId}`));
480
+ }
481
+ return result;
482
+ });
483
+ // Get aggregated statistics for a job's lint results
484
+ fastify.get('/lint/:jobId/stats', {
485
+ schema: {
486
+ description: 'Get aggregated statistics for a completed lint job, including rule breakdown and top paths. Does not return individual issues — use GET /lint/:jobId/results for that.',
487
+ tags: ['Linting'],
488
+ summary: 'Get job result statistics',
489
+ operationId: 'getJobStats',
490
+ params: jobIdParamSchema,
491
+ response: {
492
+ 200: { description: 'Aggregated job statistics' },
493
+ 404: { $ref: 'ErrorResponse#', description: 'Resource not found' },
494
+ },
495
+ },
496
+ }, async (request, reply) => {
497
+ const { jobId } = request.params;
498
+ if (storage.getJobStats) {
499
+ const stats = await storage.getJobStats(jobId);
500
+ if (!stats) {
501
+ return reply.code(404).send(createErrorResponse('Not Found', `Job not found or not yet completed: ${jobId}`));
502
+ }
503
+ return stats;
504
+ }
505
+ // Fallback: basic stats from full result
506
+ const result = await orchestrator.getJobResult(jobId);
507
+ if (!result) {
508
+ return reply.code(404).send(createErrorResponse('Not Found', `Job not found or not yet completed: ${jobId}`));
509
+ }
510
+ return {
511
+ jobId: result.jobId,
512
+ documentId: result.documentId,
513
+ rulesetName: result.rulesetName,
514
+ rulesetVersion: result.rulesetVersion,
515
+ status: result.status,
516
+ summary: result.summary,
517
+ ruleBreakdown: [],
518
+ topPaths: [],
519
+ };
520
+ });
521
+ // Generate SARIF report for completed job
522
+ fastify.post('/lint/:jobId/reports/generate', {
523
+ schema: {
524
+ description: 'Generate a structured report from a completed lint job. Currently supports SARIF (Static Analysis Results Interchange Format) output.',
525
+ tags: ['Reports'],
526
+ summary: 'Generate report for completed job',
527
+ operationId: 'generateReport',
528
+ params: jobIdParamSchema,
529
+ body: { $ref: 'ReportGenerationRequest#' },
530
+ response: {
531
+ 200: { $ref: 'SarifReport#', description: 'Generated SARIF report' },
532
+ 400: { $ref: 'ErrorResponse#', description: 'Bad request' },
533
+ 404: { $ref: 'ErrorResponse#', description: 'Resource not found' },
534
+ 500: { $ref: 'ErrorResponse#', description: 'Internal server error' },
535
+ },
536
+ },
537
+ }, async (request, reply) => {
538
+ const { jobId } = request.params;
539
+ const { format, options } = request.body;
540
+ // Validate format (only SARIF supported in Phase 1)
541
+ if (format !== 'sarif') {
542
+ return reply.code(400).send(createErrorResponse('Bad Request', `Unsupported format: ${format}. Only 'sarif' is supported.`));
543
+ }
544
+ // Get completed job result
545
+ const result = await orchestrator.getJobResult(jobId);
546
+ if (!result) {
547
+ return reply.code(404).send(createErrorResponse('Not Found', `Job not found or not yet completed: ${jobId}`));
548
+ }
549
+ if (result.status !== 'completed') {
550
+ return reply.code(400).send(createErrorResponse('Bad Request', `Job is not completed. Status: ${result.status}`));
551
+ }
552
+ // Generate SARIF report
553
+ try {
554
+ const sarif = sarifBuilder.buildSarif(result, options);
555
+ return reply.send(sarif);
556
+ }
557
+ catch (error) {
558
+ console.error('Error generating SARIF report:', error);
559
+ return reply.code(500).send(createErrorResponse('Internal Server Error', 'Failed to generate SARIF report'));
560
+ }
561
+ });
562
+ // Generate Spectral CLI reproduction instructions for a completed job
563
+ fastify.get('/lint/:jobId/reproduce', {
564
+ schema: {
565
+ description: 'Generate Markdown instructions for reproducing a lint job using the native Spectral CLI. Includes git clone, install, and spectral lint commands. When rule overrides were applied, includes Spectral override configuration.',
566
+ tags: ['Reports'],
567
+ summary: 'Get Spectral reproduction instructions',
568
+ operationId: 'getReproductionInstructions',
569
+ params: jobIdParamSchema,
570
+ response: {
571
+ 200: { type: 'string', description: 'Markdown reproduction instructions' },
572
+ 400: { $ref: 'ErrorResponse#', description: 'Job not yet completed' },
573
+ 404: { $ref: 'ErrorResponse#', description: 'Job or ruleset not found' },
574
+ },
575
+ },
576
+ }, async (request, reply) => {
577
+ const { jobId } = request.params;
578
+ // Get completed job result
579
+ const result = await orchestrator.getJobResult(jobId);
580
+ if (!result) {
581
+ return reply.code(404).send(createErrorResponse('Not Found', `Job not found or not yet completed: ${jobId}`));
582
+ }
583
+ // Must be in a terminal state
584
+ const terminalStatuses = ['completed', 'completed_with_errors', 'failed', 'timeout'];
585
+ if (!terminalStatuses.includes(result.status)) {
586
+ return reply.code(400).send(createErrorResponse('Bad Request', `Job not yet completed. Status: ${result.status}`));
587
+ }
588
+ // Get ruleset source metadata
589
+ const sourceMetadata = await rulesetLoader.getSourceMetadata(result.rulesetName, result.rulesetVersion);
590
+ if (!sourceMetadata) {
591
+ return reply.code(404).send(createErrorResponse('Not Found', `Source metadata not found for ruleset '${result.rulesetName}' version '${result.rulesetVersion}'`));
592
+ }
593
+ const markdown = generateReproductionMarkdown(result, sourceMetadata);
594
+ return reply.type('text/markdown; charset=utf-8').send(markdown);
595
+ });
596
+ // Get storage and failure statistics
597
+ fastify.get('/stats', {
598
+ schema: {
599
+ description: 'Returns storage statistics and orchestrator metrics.',
600
+ tags: ['Statistics'],
601
+ summary: 'Storage and failure statistics',
602
+ operationId: 'getStats',
603
+ response: {
604
+ 200: { $ref: 'StatsResponse#', description: 'Server statistics' },
605
+ 500: { $ref: 'ErrorResponse#', description: 'Internal server error' },
606
+ },
607
+ },
608
+ }, async () => {
609
+ const storageStats = await storage.getStats();
610
+ const orchestratorStats = orchestrator.getStats();
611
+ return {
612
+ storage: storageStats,
613
+ orchestrator: orchestratorStats,
614
+ timestamp: new Date().toISOString()
615
+ };
616
+ });
617
+ // List jobs (lightweight - documentId only)
618
+ fastify.get('/lint/jobs', {
619
+ schema: {
620
+ description: 'List lint jobs with optional filtering. Returns lightweight job summaries (document ID only, no metadata).',
621
+ tags: ['Jobs'],
622
+ summary: 'List jobs (lightweight)',
623
+ operationId: 'listJobs',
624
+ querystring: JobListQuerySchema,
625
+ response: {
626
+ 200: { $ref: 'JobListResponse#', description: 'Paginated list of jobs' },
627
+ 500: { $ref: 'ErrorResponse#', description: 'Internal server error' },
628
+ },
629
+ },
630
+ }, async (request) => {
631
+ const { status, documentId, rulesetName, startDate, endDate, limit, offset, sortBy, sortOrder } = request.query;
632
+ const options = {
633
+ ...(status && { status: status.includes(',') ? status.split(',') : status }),
634
+ ...(documentId && { documentId }),
635
+ ...(rulesetName && { rulesetName }),
636
+ ...(startDate && { startDate: new Date(startDate) }),
637
+ ...(endDate && { endDate: new Date(endDate) }),
638
+ limit: limit ? parseInt(limit, 10) : 50,
639
+ offset: offset ? parseInt(offset, 10) : 0,
640
+ sortBy: sortBy || 'timestamp',
641
+ sortOrder: sortOrder || 'desc'
642
+ };
643
+ return orchestrator.listJobs(options, runtimeSessionId);
644
+ });
645
+ // List jobs with document metadata (detailed)
646
+ fastify.get('/lint/jobs/details', {
647
+ schema: {
648
+ description: 'List lint jobs with full document metadata attached. Heavier than `/lint/jobs` but provides complete context for each job.',
649
+ tags: ['Jobs'],
650
+ summary: 'List jobs with document metadata',
651
+ operationId: 'listJobsDetailed',
652
+ querystring: JobListQuerySchema,
653
+ response: {
654
+ 200: { $ref: 'JobListDetailedResponse#', description: 'Paginated list of jobs with document metadata' },
655
+ 500: { $ref: 'ErrorResponse#', description: 'Internal server error' },
656
+ },
657
+ },
658
+ }, async (request) => {
659
+ const { status, documentId, rulesetName, startDate, endDate, limit, offset, sortBy, sortOrder } = request.query;
660
+ const options = {
661
+ ...(status && { status: status.includes(',') ? status.split(',') : status }),
662
+ ...(documentId && { documentId }),
663
+ ...(rulesetName && { rulesetName }),
664
+ ...(startDate && { startDate: new Date(startDate) }),
665
+ ...(endDate && { endDate: new Date(endDate) }),
666
+ limit: limit ? parseInt(limit, 10) : 50,
667
+ offset: offset ? parseInt(offset, 10) : 0,
668
+ sortBy: sortBy || 'timestamp',
669
+ sortOrder: sortOrder || 'desc'
670
+ };
671
+ return orchestrator.listJobsDetailed(options, runtimeSessionId);
672
+ });
673
+ // Invalidate cache for a document
674
+ fastify.delete('/cache/:documentId', {
675
+ schema: {
676
+ description: 'Remove all cached lint results for a specific document.',
677
+ tags: ['Cache'],
678
+ summary: 'Invalidate cache for a document',
679
+ operationId: 'invalidateCache',
680
+ params: documentIdParamSchema,
681
+ response: {
682
+ 200: { $ref: 'CacheInvalidationResponse#', description: 'Cache invalidated successfully' },
683
+ 500: { $ref: 'ErrorResponse#', description: 'Internal server error' },
684
+ },
685
+ },
686
+ }, async (request) => {
687
+ const { documentId } = request.params;
688
+ await orchestrator.invalidateCache(sanitizeDocumentId(documentId));
689
+ return {
690
+ message: `Cache invalidated for document: ${documentId}`
691
+ };
692
+ });
693
+ // List available rulesets
694
+ fastify.get('/rulesets', {
695
+ schema: {
696
+ description: 'Returns all configured rulesets with metadata and rule counts.',
697
+ tags: ['Rulesets'],
698
+ summary: 'List available rulesets',
699
+ operationId: 'listRulesets',
700
+ response: {
701
+ 200: {
702
+ description: 'List of available rulesets',
703
+ type: 'array',
704
+ items: { $ref: 'RulesetSummary#' },
705
+ },
706
+ 500: { $ref: 'ErrorResponse#', description: 'Internal server error' },
707
+ },
708
+ },
709
+ }, async () => {
710
+ try {
711
+ // Use the new method that loads rulesets to get accurate rule counts
712
+ const rulesets = await rulesetLoader.listRulesetsWithCounts();
713
+ // Return array directly for easier client consumption
714
+ return rulesets.map(metadata => ({
715
+ name: metadata.name,
716
+ version: metadata.defaultVersion,
717
+ defaultVersion: metadata.defaultVersion,
718
+ description: metadata.description,
719
+ availableVersions: metadata.versions,
720
+ displayName: metadata.displayName,
721
+ category: metadata.category,
722
+ ruleCount: metadata.ruleCount,
723
+ tags: metadata.tags
724
+ }));
725
+ }
726
+ catch (error) {
727
+ console.error('Error listing rulesets:', error);
728
+ throw error;
729
+ }
730
+ });
731
+ // Get ruleset details including rules
732
+ fastify.get('/rulesets/:name', {
733
+ schema: {
734
+ description: 'Retrieve detailed information about a specific ruleset, including all individual rules with their severity and descriptions.',
735
+ tags: ['Rulesets'],
736
+ summary: 'Get ruleset details including rules',
737
+ operationId: 'getRulesetDetails',
738
+ params: RulesetNameParamSchema,
739
+ querystring: RulesetVersionQuerySchema,
740
+ response: {
741
+ 200: { $ref: 'RulesetDetails#', description: 'Ruleset details with rules' },
742
+ 404: { $ref: 'ErrorResponse#', description: 'Resource not found' },
743
+ },
744
+ },
745
+ }, async (request, reply) => {
746
+ const { name } = request.params;
747
+ const { version } = request.query;
748
+ try {
749
+ const rulesetVersion = await rulesetLoader.loadVersion(name, version);
750
+ return {
751
+ name: rulesetVersion.metadata.name,
752
+ displayName: rulesetVersion.metadata.displayName,
753
+ version: rulesetVersion.version,
754
+ description: rulesetVersion.metadata.description,
755
+ category: rulesetVersion.metadata.category,
756
+ ruleCount: rulesetVersion.rules.length,
757
+ releaseDate: rulesetVersion.releaseDate,
758
+ deprecated: rulesetVersion.deprecated,
759
+ tags: rulesetVersion.metadata.tags,
760
+ rules: rulesetVersion.rules.map(rule => ({
761
+ name: rule.name,
762
+ severity: rule.severity,
763
+ message: rule.message,
764
+ description: rule.description,
765
+ recommended: rule.recommended,
766
+ }))
767
+ };
768
+ }
769
+ catch (error) {
770
+ return reply.code(404).send(createErrorResponse('Not Found', `Ruleset '${name}' not found`));
771
+ }
772
+ });
773
+ // Expose the auto-generated OpenAPI specification
774
+ fastify.get('/docs/openapi.json', {
775
+ schema: { hide: true },
776
+ }, async (_request, reply) => {
777
+ return reply.send(fastify.swagger());
778
+ });
779
+ // Start HTTP listener
780
+ const port = config.server?.port || 3003;
781
+ const host = config.server?.host || '0.0.0.0';
782
+ try {
783
+ await fastify.listen({ port, host });
784
+ console.log(`\n✅ Orchestrator service v${API_VERSION}`);
785
+ console.log(` HTTP API listening on http://${host}:${port}`);
786
+ console.log(`\nAvailable endpoints:`);
787
+ console.log(` POST /documents - Upload OpenAPI document`);
788
+ console.log(` GET /documents - List/search documents`);
789
+ console.log(` GET /documents/:documentId - Get document by ID`);
790
+ console.log(` POST /lint - Submit lint job`);
791
+ console.log(` GET /lint/:jobId - Get job status`);
792
+ console.log(` GET /lint/:jobId/results - Get job results`);
793
+ console.log(` GET /lint/jobs - List jobs (lightweight)`);
794
+ console.log(` GET /lint/jobs/details - List jobs (with document metadata)`);
795
+ console.log(` POST /lint/:jobId/reports/generate - Generate SARIF report`);
796
+ console.log(` GET /lint/:jobId/reproduce - Spectral CLI reproduction instructions`);
797
+ console.log(` DELETE /cache/:documentId - Invalidate cache`);
798
+ console.log(` GET /rulesets - List available rulesets`);
799
+ console.log(` GET /rulesets/:name - Get ruleset details`);
800
+ console.log(` GET /stats - Storage & failure statistics`);
801
+ console.log(` GET /stats/orchestrator - Orchestrator statistics`);
802
+ console.log(` GET /health - Health check`);
803
+ console.log(` GET /docs/openapi.json - OpenAPI specification (auto-generated)`);
804
+ console.log(`\n🎯 Ready to accept lint requests!\n`);
805
+ }
806
+ catch (error) {
807
+ console.error('Failed to start HTTP server:', error);
808
+ throw error;
809
+ }
810
+ // Create shutdown handler
811
+ const shutdown = async () => {
812
+ console.log('\n🛑 Shutting down the Orchestrator service...');
813
+ try {
814
+ await orchestrator.shutdown();
815
+ await workerPool.shutdown();
816
+ await fastify.close();
817
+ console.log('✅ Shutdown complete');
818
+ }
819
+ catch (error) {
820
+ console.error('Error during shutdown:', error);
821
+ throw error;
822
+ }
823
+ };
824
+ // Return server instance with control interface
825
+ return {
826
+ fastify,
827
+ orchestrator,
828
+ workerPool,
829
+ documentStore,
830
+ config,
831
+ shutdown
832
+ };
833
+ }
834
+ //# sourceMappingURL=server.js.map