(config = loadConfig())
| 24 | import { createQuestionLog } from './storage/questionLog.js'; |
| 25 | |
| 26 | export { loadConfig, ensureContentRoot, defaultContentRoot } from './config.js'; |
| 27 | export type { ServerConfig } from './config.js'; |
| 28 | |
| 29 | export interface CreateServerOptions { |
| 30 | /** |
| 31 | * Override the RocketReach client factory. Production leaves this unset (the |
| 32 | * real HTTPS client is used); tests inject a stub / a client with a fake |
| 33 | * `fetch` so the suite never touches the network. |
| 34 | */ |
| 35 | createRocketReachClient?: (options: RocketReachClientOptions) => RocketReachClient; |
| 36 | } |
| 37 | |
| 38 | export function createServer( |
| 39 | config = loadConfig(), |
| 40 | options: CreateServerOptions = {}, |
| 41 | ): http.Server { |
| 42 | ensureContentRoot(config.contentRoot); |
| 43 | const pathResolver = createPathResolver(config.contentRoot); |
| 44 | const repository = createFileRepository(pathResolver); |
| 45 | const auditLog = createAuditLog(config.contentRoot); |
| 46 | const proposalStore = createProposalStore(config.contentRoot); |
| 47 | const questionLog = createQuestionLog(config.contentRoot); |
| 48 | const integrationStore = createIntegrationStore(config.contentRoot); |
| 49 | const patchIdempotency = createIdempotencyCache<PatchFileResponse>(); |
| 50 | const eventBus = createEventBus(); |
| 51 | // Surface out-of-band edits (direct file writes, git, another process) so the |
| 52 | // human's view stays live even for changes that never hit the API. |
| 53 | const watcher = createVaultWatcher({ contentRoot: config.contentRoot, eventBus }); |
| 54 | // A cached retrieval index so search / semantic / context don't re-read the |
| 55 | // whole vault per query. It subscribes to the same bus, so any write (API or |
| 56 | // out-of-band) invalidates it and the next query rebuilds — never stale. |
| 57 | const vaultIndex = createVaultIndex({ repository, eventBus, contentRoot: config.contentRoot }); |
| 58 | |
| 59 | // Optional "dream cycle": when MAINTENANCE_INTERVAL_MS is a positive number, |
| 60 | // periodically scan the vault for hygiene problems (broken links, orphans, |
| 61 | // near-duplicates) and file each fix as a proposal for human review. Off by |
| 62 | // default (unset); on-demand scanning is always available via the endpoints. |
| 63 | const maintenanceIntervalMs = Number(process.env.MAINTENANCE_INTERVAL_MS); |
| 64 | let maintenanceTimer: NodeJS.Timeout | undefined; |
| 65 | if (Number.isFinite(maintenanceIntervalMs) && maintenanceIntervalMs > 0) { |
| 66 | maintenanceTimer = setInterval(() => { |
| 67 | void runMaintenanceScan({ vaultIndex, proposalStore, eventBus, pathResolver }).catch(() => { |
| 68 | // Best-effort: a scheduled scan must never crash the server. |
| 69 | }); |
| 70 | }, maintenanceIntervalMs); |
| 71 | // Don't keep the process (or a test's event loop) alive just for the scan. |
| 72 | maintenanceTimer.unref?.(); |
| 73 | } |
| 74 | |
| 75 | // Optional outreach feedback loop: when FEEDBACK_INTERVAL_MS is a positive |
| 76 | // number, periodically compare reviewed draft→final pairs and file each |
| 77 | // distilled lesson as a proposal for human review. Off by default; on-demand |
| 78 | // scanning is always available via `POST /api/feedback/scan`. |
| 79 | const feedbackIntervalMs = Number(process.env.FEEDBACK_INTERVAL_MS); |
| 80 | let feedbackTimer: NodeJS.Timeout | undefined; |
| 81 | if (Number.isFinite(feedbackIntervalMs) && feedbackIntervalMs > 0) { |
| 82 | feedbackTimer = setInterval(() => { |
| 83 | void runFeedbackScan({ vaultIndex, proposalStore, eventBus, pathResolver }).catch(() => { |
no test coverage detected