@sylphx/sdk 0.5.0 → 0.8.0-rc.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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/health/effects.ts","../../src/health/types.ts","../../src/health/handler.ts","../../src/health/scoring.ts","../../src/health/signals/event-loop-lag.ts","../../src/health/unix-socket-server.ts","../../src/health/signals/error-rate.ts","../../src/health/signals/memory-pressure.ts","../../src/health/signals/queue-depth.ts","../../src/health/index.ts"],"sourcesContent":["/**\n * Effect TS integration for `@sylphx/sdk/health` (Rule 21 / ADR-058 Amendment).\n *\n * Effect-native services should NEVER call `Effect.runPromise` inside\n * business logic — this module exposes `evaluateEffect` so they can fold\n * the health computation into their own fiber graph.\n *\n * `effect` is an OPTIONAL peer dependency — apps that don't import this\n * module never pull it in (sideEffects: false + tree-shaking guarantees\n * this; verified in tests). The `Effect` type is imported from the\n * peer-dep at runtime; consumers either provide it or never reach this\n * code path.\n */\n\nimport { Effect } from 'effect'\nimport type { HealthError, HealthScore } from './types'\nimport { HealthError as HealthErrorClass } from './types'\n\n/**\n * Lift a `() => Promise<HealthScore>` evaluator into an Effect-native value.\n *\n * Errors thrown by the evaluator are tagged `HealthError` so callers can\n * use `Effect.catchTag('HealthError', …)` for typed recovery.\n *\n * @example\n * ```ts\n * import { Effect } from 'effect'\n * import { sylphxHealth } from '@sylphx/sdk/health'\n *\n * const health = sylphxHealth({ signals: [...] })\n *\n * const program = Effect.gen(function* () {\n * const { score, signals } = yield* health.evaluateEffect\n * yield* Effect.log(`health=${score.toFixed(2)} signals=${JSON.stringify(signals)}`)\n * return score\n * })\n *\n * // Only the entry point runs the Effect (Rule 21).\n * const finalScore = await Effect.runPromise(program)\n * ```\n */\nexport function evaluateEffect(\n\tevaluator: () => Promise<HealthScore>,\n): Effect.Effect<HealthScore, HealthError, never> {\n\treturn Effect.tryPromise({\n\t\ttry: () => evaluator(),\n\t\tcatch: (err) => new HealthErrorClass('health evaluation failed', err),\n\t})\n}\n","/**\n * Type SSOT for `@sylphx/sdk/health` (ADR-111 §4).\n *\n * Pure runtime types — framework-free, no schema library required at the\n * SDK boundary. Apps that want Standard Schema validation (per ADR-084)\n * can wrap the wire-format `HealthSnapshot` in their own schema.\n *\n * The wire shape (`HealthSnapshot`) is the **stable contract** the\n * `sylphx-health-agent` sidecar parses (see ADR-111 §3.2.4 +\n * `apps/health-agent/src/app-poll.ts::parseAppHealthBody`). Do not break it.\n */\n\n// ─── Signal — what an app registers ─────────────────────────────────────────\n\n/**\n * One reading produced by a `Signal.read()`. The aggregator turns each\n * reading into a `factor` in `[0, 1]` then folds them into the score via\n * the configured `ScoringStrategy`.\n *\n * `value` is the raw observation (ms, ratio, count, …) — surfaced verbatim\n * in the wire snapshot so operators can debug from JSON without re-running\n * the signal logic.\n *\n * `healthFactor` is the normalised health in `[0, 1]`:\n * - `1` = fully healthy\n * - `0` = dead (or \"we don't know\" if `unknown=true` and the policy says so)\n *\n * `unknown=true` means the signal **could not be measured this tick** (e.g.\n * cgroup file unreadable). Scoring strategies treat unknown signals as\n * `factor=1` (don't penalise an app for our own missing data).\n */\nexport interface SignalReading {\n\treadonly value: number | string | boolean\n\treadonly healthFactor: number\n\treadonly unknown?: boolean\n}\n\n/**\n * A health signal — one named, weighted measurement the score is built from.\n *\n * Two variants:\n * - `SyncSignal` — `read(): SignalReading` (e.g. event-loop lag)\n * - `AsyncSignal` — `read(): Promise<SignalReading>` (e.g. queue depth\n * fetched over IPC)\n *\n * The discriminated `Signal` union accepts both. The aggregator awaits\n * each reading uniformly via `Promise.resolve(signal.read())`.\n *\n * Implementations are pure — no internal mutation outside the closure-\n * captured monitor state (e.g. `monitorEventLoopDelay()`). Stop /\n * cleanup are exposed via `dispose()` for tests and graceful shutdown.\n */\nexport interface SignalBase {\n\t/** Unique stable identifier; appears in the wire `signals.<name>` map. */\n\treadonly name: string\n\t/**\n\t * Weight in the weighted-product score. Strictly `> 0` for active\n\t * signals; `0` is a no-op signal (kept for compatibility with\n\t * conditional registration but skipped in scoring).\n\t */\n\treadonly weight: number\n\t/**\n\t * Tear down any background work (timers, monitors, file watchers).\n\t * Called during graceful shutdown and from test cleanup.\n\t */\n\tdispose?(): void\n}\nexport interface SyncSignal extends SignalBase {\n\tread(): SignalReading\n}\nexport interface AsyncSignal extends SignalBase {\n\tread(): Promise<SignalReading>\n}\nexport type Signal = SyncSignal | AsyncSignal\n\n// ─── Score — what the sidecar reads ─────────────────────────────────────────\n\n/**\n * The complete health score + signal breakdown produced by `health.evaluate()`.\n *\n * `score` is the normalised aggregate in `[0, 1]`. Signal payload is\n * verbatim values from each `SignalReading.value` so operators can\n * cross-reference Grafana dashboards with the JSON.\n */\nexport interface HealthScore {\n\treadonly score: number\n\treadonly signals: Record<string, number | string | boolean>\n\treadonly lastTickAt: string\n}\n\n/**\n * Wire format the sidecar's `app-poll.ts::parseAppHealthBody` consumes.\n *\n * Pinned by ADR-111 §3.2.4 — keep stable. The sidecar tolerates extra\n * keys; do NOT remove existing ones without sequencing the sidecar update\n * first.\n */\nexport type HealthSnapshot = HealthScore\n\n// ─── Errors ─────────────────────────────────────────────────────────────────\n\n/**\n * Tagged error type for the Effect API. Promise consumers see the same\n * `message` via `Error.message` — the tag is for `Effect.catchTag`.\n */\nexport class HealthError extends Error {\n\treadonly _tag = 'HealthError' as const\n\treadonly cause?: unknown\n\tconstructor(message: string, cause?: unknown) {\n\t\tsuper(message)\n\t\tthis.name = 'HealthError'\n\t\tthis.cause = cause\n\t}\n}\n\n// ─── Scoring strategy ───────────────────────────────────────────────────────\n\n/**\n * `ScoringStrategy` collapses N readings → one `score` in `[0, 1]`.\n *\n * Default is `weightedProduct` (see `scoring.ts`). Custom strategies can\n * be plugged in via `sylphxHealth({ scoringStrategy: myStrategy })`.\n */\nexport type ScoringStrategy = (\n\treadings: ReadonlyArray<{ signal: Signal; reading: SignalReading }>,\n) => number\n\n// ─── Public option types ────────────────────────────────────────────────────\n\nexport interface SylphxHealthOptions {\n\t/**\n\t * Signals to register. If omitted, defaults to a single\n\t * `eventLoopLagSignal({ degradedMs: 5000, deadMs: 30000 })` per\n\t * ADR-111 §4.6 (\"sane out-of-box\").\n\t */\n\treadonly signals?: ReadonlyArray<Signal>\n\t/**\n\t * Strategy used to fold readings into a score. Defaults to\n\t * `weightedProduct` from `scoring.ts`.\n\t */\n\treadonly scoringStrategy?: ScoringStrategy\n\t/**\n\t * Optional injected clock — used by tests for deterministic\n\t * `lastTickAt` timestamps.\n\t */\n\treadonly now?: () => Date\n}\n","/**\n * Universal HTTP handler for `@sylphx/sdk/health` (ADR-111 §4.2).\n *\n * Framework-agnostic: returns either a `Web Fetch API` `Response`\n * (Hono / Bun.serve / itty-router / Next.js routes) or, via thin adapters,\n * a Node `(req, res) => void` (Express / Fastify-as-classic-handler).\n *\n * The handler always returns **HTTP 200** unless the SDK itself failed to\n * compute a score (rare; caught and returned as 500 for ops to triage).\n * The three-tier 200 / 503 gate (ADR-111 §4.4) lives in the **sidecar**,\n * not here — the SDK's job ends at exposing the score so the sidecar\n * can decide. This separation is documented in the README (\"apps don't\n * need to think about probe semantics\").\n */\n\nimport type { HealthScore } from './types'\n\n/**\n * Minimal contract a `health` instance must expose for the handler to\n * call. The full `health` object satisfies this implicitly via\n * `evaluate()` from `./index.ts`.\n */\nexport interface HealthEvaluator {\n\tevaluate(): Promise<HealthScore>\n}\n\n/**\n * Build a Web-Fetch-style handler. Suitable for:\n * - Hono: `app.get('/healthz', health.handler())`\n * - Bun.serve: `fetch: health.handler()`\n * - Next.js route.ts: `export const GET = health.handler()`\n * - itty-router / Hattip / standard `(Request) => Response` runtimes\n */\nexport function createWebHandler(source: HealthEvaluator): (req?: Request) => Promise<Response> {\n\treturn async (_req?: Request): Promise<Response> => {\n\t\ttry {\n\t\t\tconst score = await source.evaluate()\n\t\t\treturn new Response(JSON.stringify(score), {\n\t\t\t\tstatus: 200,\n\t\t\t\theaders: {\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t// Cache-Control: never cache. Even 1-second cache on a\n\t\t\t\t\t// CDN edge would mask a fast-moving health collapse.\n\t\t\t\t\t'Cache-Control': 'no-store, no-cache, must-revalidate',\n\t\t\t\t},\n\t\t\t})\n\t\t} catch (err) {\n\t\t\tconst message = err instanceof Error ? err.message : String(err)\n\t\t\treturn new Response(\n\t\t\t\tJSON.stringify({\n\t\t\t\t\terror: 'health_evaluator_failed',\n\t\t\t\t\tmessage,\n\t\t\t\t}),\n\t\t\t\t{\n\t\t\t\t\tstatus: 500,\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t'Cache-Control': 'no-store, no-cache, must-revalidate',\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t)\n\t\t}\n\t}\n}\n\n/**\n * Build a Node-style `(req, res)` handler — for Express / classic Fastify.\n *\n * Adapter that delegates to `createWebHandler()` so logic stays in one\n * place. Imported lazily by callers that need it; doesn't drag any node\n * types into Web-only code paths.\n */\nexport interface NodeIncoming {\n\tmethod?: string\n\turl?: string\n}\nexport interface NodeOutgoing {\n\tstatusCode: number\n\tsetHeader(name: string, value: string): void\n\tend(body?: string): void\n}\nexport function createNodeHandler(\n\tsource: HealthEvaluator,\n): (req: NodeIncoming, res: NodeOutgoing) => Promise<void> {\n\treturn async (_req, res) => {\n\t\ttry {\n\t\t\tconst score = await source.evaluate()\n\t\t\tres.statusCode = 200\n\t\t\tres.setHeader('Content-Type', 'application/json')\n\t\t\tres.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate')\n\t\t\tres.end(JSON.stringify(score))\n\t\t} catch (err) {\n\t\t\tconst message = err instanceof Error ? err.message : String(err)\n\t\t\tres.statusCode = 500\n\t\t\tres.setHeader('Content-Type', 'application/json')\n\t\t\tres.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate')\n\t\t\tres.end(JSON.stringify({ error: 'health_evaluator_failed', message }))\n\t\t}\n\t}\n}\n","/**\n * Scoring strategies (ADR-111 §4 — three-tier health gate).\n *\n * The default `weightedProduct` strategy is multiplicative — a single bad\n * signal can drag the whole score down (any factor of 0 → score 0). This is\n * the right semantic for **liveness**: one critical subsystem dead means\n * the pod is not ready, regardless of how healthy the rest is.\n *\n * Mathematics:\n * score = ∏ factor_i ^ weight_i (over signals where weight > 0 AND\n * reading is not `unknown`)\n *\n * Where weights are normalised to sum=1 first, so absolute weight values\n * don't matter — only the ratios do. This keeps user expectations sane:\n * `[w=2, w=2]` and `[w=10, w=10]` produce identical scores.\n *\n * Edge cases (deterministic, never throw):\n * - empty input → 1 (perfect health, nothing to penalise)\n * - all weights = 0 → 1 (no active signals)\n * - all readings `unknown` → 1 (we can't see; cardinal-rule fallback\n * lives at the sidecar boundary, not\n * here — ADR-111 §3.2.5)\n * - factor < 0 / NaN → clamped to 0\n * - factor > 1 → clamped to 1\n * - weight < 0 → clamped to 0 (ignored)\n *\n * The clamps make us **safe by construction** — a misconfigured signal\n * cannot push the score outside `[0, 1]`. Tests exercise all branches.\n */\n\nimport type { ScoringStrategy, Signal, SignalReading } from './types'\n\n/** Clamp `x` to `[0, 1]`, mapping NaN to 0. */\nfunction clamp01(x: number): number {\n\tif (!Number.isFinite(x)) return 0\n\tif (x < 0) return 0\n\tif (x > 1) return 1\n\treturn x\n}\n\n/**\n * Weighted geometric mean — the default scoring strategy.\n *\n * @example\n * weightedProduct([\n * { signal: { name: 'lag', weight: 0.4 }, reading: { healthFactor: 0.4 } },\n * { signal: { name: 'q', weight: 0.6 }, reading: { healthFactor: 1.0 } },\n * ])\n * // → 0.4^0.4 × 1.0^0.6 ≈ 0.693 — ADR-111 §4.5 worked example\n */\nexport const weightedProduct: ScoringStrategy = (readings) => {\n\tif (readings.length === 0) return 1\n\n\t// 1. Filter active signals (weight > 0, reading defined, not unknown).\n\tconst active: Array<{ factor: number; weight: number }> = []\n\tfor (const { signal, reading } of readings) {\n\t\tconst w = signal.weight\n\t\tif (!Number.isFinite(w) || w <= 0) continue\n\t\tif (reading.unknown === true) continue\n\t\tactive.push({\n\t\t\tfactor: clamp01(reading.healthFactor),\n\t\t\tweight: w,\n\t\t})\n\t}\n\tif (active.length === 0) return 1\n\n\t// 2. Normalise weights to sum=1.\n\tconst totalWeight = active.reduce((sum, a) => sum + a.weight, 0)\n\tif (totalWeight <= 0 || !Number.isFinite(totalWeight)) return 1\n\n\t// 3. Geometric mean: exp(Σ w_i × ln(f_i)) — exp/log is more numerically\n\t// stable than repeated `Math.pow` × multiplication for small factors.\n\t// ln(0) → -Infinity → exp(-Infinity) → 0, which is exactly what we\n\t// want (any dead signal kills the score).\n\tlet logSum = 0\n\tfor (const { factor, weight } of active) {\n\t\tconst normalisedWeight = weight / totalWeight\n\t\tif (factor <= 0) {\n\t\t\t// Short-circuit: a single zero factor => score 0, regardless of\n\t\t\t// the others. Avoids ln(0) sentinel value plumbing.\n\t\t\treturn 0\n\t\t}\n\t\tlogSum += normalisedWeight * Math.log(factor)\n\t}\n\tconst score = Math.exp(logSum)\n\treturn clamp01(score)\n}\n\n/**\n * Convenience: build a default scoring strategy.\n *\n * Reserved for future: weighted-min, weighted-mean, etc. For now there's\n * one strategy and `weightedProduct` is the only export — this keeps the\n * surface narrow until a concrete second use-case appears.\n */\nexport function defaultScoringStrategy(): ScoringStrategy {\n\treturn weightedProduct\n}\n\n// Re-export the types used by callers writing custom strategies.\nexport type { ScoringStrategy, Signal, SignalReading }\n","/**\n * `eventLoopLagSignal` — main-thread blocking detector (ADR-111 §4.3).\n *\n * Measures Node/Bun's libuv event-loop delay using `monitorEventLoopDelay()`\n * from `node:perf_hooks`. The monitor is a histogram updated by libuv at a\n * configurable resolution (default 10 ms here — every tick measures lag\n * between expected wake and actual wake). We report the **max** observed\n * since the last `read()` then `reset()` — that's the worst-case stall\n * the app suffered in the polling window.\n *\n * Mapping observed-lag-ms → healthFactor ∈ [0, 1]:\n *\n * healthFactor\n * ▲\n * 1.0 ┤━━━━━━━━━┓\n * │ ┃ linear interpolation\n * │ ┃\n * 0.0 ┤ ┗━━━━━━━━━━━━━\n * ┼─────────┼──────────┼──────► observed lag (ms)\n * 0 degradedMs deadMs\n *\n * Below `degradedMs` → factor 1 (healthy). Above `deadMs` → factor 0 (dead).\n * In-between → linear interpolation. ADR-111 §4.3 default thresholds:\n * degradedMs = 5000 (5 s — same order as ADR-110's 10 s probe timeout)\n * deadMs = 30000 (30 s — definitely wedged)\n *\n * Bun-compatibility: Bun ships `monitorEventLoopDelay()` with the same\n * shape as Node 16+. Verified against `bun:1.3` in this repo's CI.\n */\n\nimport { monitorEventLoopDelay } from 'node:perf_hooks'\nimport type { SignalReading, SyncSignal } from '../types'\n\nexport interface EventLoopLagOptions {\n\t/** Lag below this is fully healthy (factor=1). Default 5000 ms. */\n\treadonly degradedMs?: number\n\t/** Lag above this is fully dead (factor=0). Default 30000 ms. */\n\treadonly deadMs?: number\n\t/**\n\t * Histogram resolution (ms). Default 10 ms — the smaller, the more\n\t * accurate the max but the higher the libuv accounting overhead.\n\t * Node defaults are also 10 ms.\n\t */\n\treadonly resolutionMs?: number\n\t/** Weight in the weighted-product score. Default 0.4 (ADR-111 §4.3). */\n\treadonly weight?: number\n\t/**\n\t * Optional injected monitor (tests). Defaults to a fresh\n\t * `monitorEventLoopDelay()` instance.\n\t */\n\treadonly monitor?: ReturnType<typeof monitorEventLoopDelay>\n}\n\n/**\n * Build an `event-loop-lag` signal. The returned object owns a started\n * histogram monitor; call `dispose()` during shutdown / between tests.\n */\nexport function eventLoopLagSignal(opts: EventLoopLagOptions = {}): SyncSignal {\n\tconst degradedMs = opts.degradedMs ?? 5000\n\tconst deadMs = opts.deadMs ?? 30000\n\tconst resolutionMs = opts.resolutionMs ?? 10\n\tconst weight = opts.weight ?? 0.4\n\n\tif (!Number.isFinite(degradedMs) || degradedMs < 0) {\n\t\tthrow new Error(`eventLoopLagSignal: degradedMs must be >= 0, got ${degradedMs}`)\n\t}\n\tif (!Number.isFinite(deadMs) || deadMs <= degradedMs) {\n\t\tthrow new Error(\n\t\t\t`eventLoopLagSignal: deadMs must be > degradedMs (${degradedMs}), got ${deadMs}`,\n\t\t)\n\t}\n\tif (!Number.isFinite(resolutionMs) || resolutionMs < 1) {\n\t\tthrow new Error(`eventLoopLagSignal: resolutionMs must be >= 1, got ${resolutionMs}`)\n\t}\n\n\tconst monitor = opts.monitor ?? monitorEventLoopDelay({ resolution: resolutionMs })\n\tmonitor.enable()\n\n\treturn {\n\t\tname: 'eventLoopLagMs',\n\t\tweight,\n\t\tread(): SignalReading {\n\t\t\t// `max` is in nanoseconds.\n\t\t\tconst maxNs = monitor.max\n\t\t\t// On a brand-new monitor or after `reset()` the histogram can\n\t\t\t// report `max=0` (no samples yet) or, on some Node versions,\n\t\t\t// `Number.MAX_SAFE_INTEGER` as a sentinel. Treat both as\n\t\t\t// \"no reading yet\" → unknown.\n\t\t\tif (!Number.isFinite(maxNs) || maxNs <= 0 || maxNs >= Number.MAX_SAFE_INTEGER) {\n\t\t\t\tmonitor.reset()\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tconst observedMs = maxNs / 1_000_000\n\t\t\tmonitor.reset()\n\n\t\t\tlet factor: number\n\t\t\tif (observedMs <= degradedMs) factor = 1\n\t\t\telse if (observedMs >= deadMs) factor = 0\n\t\t\telse {\n\t\t\t\t// Linear interpolation between degraded (1) and dead (0).\n\t\t\t\tconst span = deadMs - degradedMs\n\t\t\t\tfactor = 1 - (observedMs - degradedMs) / span\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tvalue: Math.round(observedMs),\n\t\t\t\thealthFactor: factor,\n\t\t\t}\n\t\t},\n\t\tdispose(): void {\n\t\t\tmonitor.disable()\n\t\t},\n\t}\n}\n","/**\n * Unix-socket server for `@sylphx/sdk/health` (ADR-111 §3.2.4 + §4.2).\n *\n * The sidecar polls the app over a Unix socket — `/var/run/sylphx/health.sock`\n * by default. The shared volume (`emptyDir` mount, see ADR-111 §3.2.2) is\n * provisioned by the reconciler when the sidecar is injected. This server\n * binds Bun's native `Bun.serve({ unix: <path> })` and serves the same\n * JSON the HTTP handler does.\n *\n * Graceful shutdown is essential — the socket file lingers on disk if not\n * cleaned, and the sidecar's first reconnect attempt after a redeploy\n * would hit a stale inode. `shutdown()` calls `server.stop()` AND\n * unlinks the socket file. The default `process.on('SIGTERM')` hook\n * inside `serveUnixSocket()` opt-in via `installSignalHandlers: true`.\n */\n\nimport { unlinkSync } from 'node:fs'\nimport { createWebHandler, type HealthEvaluator } from './handler'\n\n/** Bun-typed minimum for the server we need to control. */\ninterface BunServer {\n\tstop(force?: boolean): void\n\turl?: { href: string } | null\n}\n\nexport interface UnixSocketServerOptions {\n\t/** Absolute path to bind. Defaults to `/var/run/sylphx/health.sock`. */\n\treadonly path?: string\n\t/**\n\t * If `true`, register `SIGTERM` / `SIGINT` handlers that call\n\t * `shutdown()`. Default `false` — apps usually own signal handling\n\t * already; opt in only for standalone health-only processes.\n\t */\n\treadonly installSignalHandlers?: boolean\n\t/**\n\t * Override unlink behavior — useful for tests where the test runner\n\t * already owns the socket cleanup.\n\t */\n\treadonly unlinkOnShutdown?: boolean\n\t/**\n\t * Override the Bun runtime resolver. Used by tests to verify the\n\t * \"no-Bun\" error path without leaving a Bun-only test environment.\n\t * Production code reads `globalThis.Bun` directly.\n\t */\n\treadonly bunRuntime?: { serve: (cfg: unknown) => BunServer } | null\n}\n\nexport interface UnixSocketServerHandle {\n\treadonly path: string\n\treadonly server: BunServer\n\t/** Stop the server AND unlink the socket file (unless `unlinkOnShutdown=false`). */\n\tshutdown(): Promise<void>\n}\n\n/**\n * Start a Bun unix-socket HTTP server bound to `opts.path`. Returns the\n * handle so tests / shutdown handlers can call `shutdown()`.\n *\n * Pre-binds the socket: if a stale file from a prior crash exists at the\n * path, we `unlinkSync` it first so the bind doesn't `EADDRINUSE`. This\n * is safe because the path is operator-controlled (default lives under\n * `/var/run/sylphx/`, owned by the pod's UID 1000).\n */\nexport function startUnixSocketServer(\n\tsource: HealthEvaluator,\n\topts: UnixSocketServerOptions = {},\n): UnixSocketServerHandle {\n\t// Lazy reference to globalThis.Bun — keeps the import free of build-time\n\t// requirements on Bun for non-Bun consumers (server is no-op there).\n\t// Tests inject `opts.bunRuntime: null` to exercise the no-Bun path.\n\tconst bun =\n\t\topts.bunRuntime !== undefined\n\t\t\t? opts.bunRuntime\n\t\t\t: (globalThis as unknown as { Bun?: { serve: (cfg: unknown) => BunServer } }).Bun\n\tif (bun === null || bun === undefined || typeof bun.serve !== 'function') {\n\t\tthrow new Error(\n\t\t\t'startUnixSocketServer: Bun.serve is unavailable — Unix-socket transport requires a Bun runtime',\n\t\t)\n\t}\n\n\tconst path = opts.path ?? '/var/run/sylphx/health.sock'\n\tconst unlinkOnShutdown = opts.unlinkOnShutdown ?? true\n\n\t// Best-effort pre-cleanup of stale socket.\n\ttry {\n\t\tunlinkSync(path)\n\t} catch {\n\t\t// ENOENT is fine; anything else means the user has the wrong path\n\t\t// or perms — the bind below will surface a clear error.\n\t}\n\n\tconst handler = createWebHandler(source)\n\tconst server = bun.serve({\n\t\tunix: path,\n\t\tfetch: async (req: Request): Promise<Response> => handler(req),\n\t}) as BunServer\n\n\tconst handle: UnixSocketServerHandle = {\n\t\tpath,\n\t\tserver,\n\t\tasync shutdown(): Promise<void> {\n\t\t\ttry {\n\t\t\t\tserver.stop(true)\n\t\t\t} catch {\n\t\t\t\t// stopping a server that's already stopped is benign\n\t\t\t}\n\t\t\tif (unlinkOnShutdown) {\n\t\t\t\ttry {\n\t\t\t\t\tunlinkSync(path)\n\t\t\t\t} catch {\n\t\t\t\t\t// ENOENT — already gone, fine\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t}\n\n\tif (opts.installSignalHandlers === true) {\n\t\tconst onSignal = (sig: string): void => {\n\t\t\tvoid handle.shutdown().then(() => {\n\t\t\t\t// 0 = clean exit; signal handlers fire only when the\n\t\t\t\t// owning process is the standalone health server, not\n\t\t\t\t// when embedded in a larger app.\n\t\t\t\tif (typeof process !== 'undefined') process.exit(0)\n\t\t\t})\n\t\t\tvoid sig\n\t\t}\n\t\tprocess.on('SIGTERM', () => onSignal('SIGTERM'))\n\t\tprocess.on('SIGINT', () => onSignal('SIGINT'))\n\t}\n\n\treturn handle\n}\n","/**\n * `errorRateSignal` — request-error-rate over a sliding window (ADR-111 §4.3).\n *\n * Phase B uses an in-memory ring buffer of {timestamp, isError} samples.\n * Phase C will replace this with an OTel collector subscription so the\n * sidecar gets the same data without per-process bookkeeping; for now the\n * in-memory path keeps the SDK self-contained.\n *\n * Mapping observed-rate → healthFactor:\n *\n * healthFactor\n * ▲\n * 1.0 ┤━━━━━━━━━━━━━━━━━━━┓\n * │ ┃ linear interpolation\n * 0.0 ┤ ┗━━━━━━━━━━\n * ┼───────────────────┼────────► error rate (0..1)\n * 0 degradedRate deadRate\n *\n * Default thresholds match ADR-111 §4.3 row 3:\n * degradedRate = 0.05 (5 % errors → degraded)\n * deadRate = 0.50 (50 % errors → dead)\n *\n * `recordSuccess()` / `recordError()` are pushed by the app on each\n * request (or wired into Hono / Express middleware — the SDK provides\n * primitives, not framework integrations). Zero-traffic windows produce\n * `factor=1` (no requests = no errors = healthy by convention).\n */\n\nimport type { SignalReading, SyncSignal } from '../types'\n\nexport interface ErrorRateOptions {\n\t/** Sliding window length. Accepts ms (number) or '5s' / '1m' shorthand. */\n\treadonly window: number | `${number}s` | `${number}m`\n\t/** Rate above this is considered degraded. Default 0.05 (5 %). */\n\treadonly degradedRate?: number\n\t/** Rate at which the signal saturates to dead. Default 0.50 (50 %). */\n\treadonly deadRate?: number\n\t/**\n\t * Soft minimum sample count: until this many samples land, factor=1\n\t * regardless of rate (avoids \"1 error in 1 request → 100 %\" panics).\n\t * Default 10.\n\t */\n\treadonly minSamples?: number\n\t/** Weight in the weighted-product score. Default 0.2 (ADR-111 §4.3). */\n\treadonly weight?: number\n\t/** Optional injected clock for tests. */\n\treadonly now?: () => number\n}\n\nexport interface ErrorRateSignalHandle extends SyncSignal {\n\t/** Record a successful request (call from app middleware). */\n\trecordSuccess(): void\n\t/** Record a failed request (call from app middleware). */\n\trecordError(): void\n\t/** Erase the window. Useful for tests. */\n\treset(): void\n}\n\ninterface Sample {\n\treadonly t: number\n\treadonly e: boolean\n}\n\nfunction parseWindow(w: ErrorRateOptions['window']): number {\n\tif (typeof w === 'number') {\n\t\tif (!Number.isFinite(w) || w <= 0) {\n\t\t\tthrow new Error(`errorRateSignal: window must be > 0, got ${w}`)\n\t\t}\n\t\treturn w\n\t}\n\tconst m = /^(\\d+)(s|m)$/.exec(w)\n\tif (m === null) {\n\t\tthrow new Error(`errorRateSignal: invalid window '${w}', expected '5s' / '1m' / number`)\n\t}\n\tconst n = Number.parseInt(m[1] as string, 10)\n\tconst unit = m[2]\n\tconst ms = unit === 's' ? n * 1000 : n * 60_000\n\tif (ms <= 0) {\n\t\tthrow new Error(`errorRateSignal: parsed window ${ms}ms must be > 0`)\n\t}\n\treturn ms\n}\n\nexport function errorRateSignal(opts: ErrorRateOptions): ErrorRateSignalHandle {\n\tconst windowMs = parseWindow(opts.window)\n\tconst degradedRate = opts.degradedRate ?? 0.05\n\tconst deadRate = opts.deadRate ?? 0.5\n\tconst minSamples = opts.minSamples ?? 10\n\tconst weight = opts.weight ?? 0.2\n\tconst now = opts.now ?? Date.now\n\n\tif (degradedRate < 0 || degradedRate > 1) {\n\t\tthrow new Error(`errorRateSignal: degradedRate must be in [0, 1], got ${degradedRate}`)\n\t}\n\tif (deadRate <= degradedRate || deadRate > 1) {\n\t\tthrow new Error(`errorRateSignal: deadRate must be in (degradedRate, 1], got ${deadRate}`)\n\t}\n\tif (!Number.isFinite(minSamples) || minSamples < 1) {\n\t\tthrow new Error(`errorRateSignal: minSamples must be >= 1, got ${minSamples}`)\n\t}\n\n\t// Linked-list-style ring; we shift the head when entries age out. For\n\t// fleets at 100 req/s with a 5 s window that's ~500 entries — well\n\t// within memory budget. Phase C swaps this for the OTel subscription.\n\tconst samples: Sample[] = []\n\n\tfunction pruneExpired(t: number): void {\n\t\tconst cutoff = t - windowMs\n\t\twhile (samples.length > 0 && (samples[0] as Sample).t < cutoff) {\n\t\t\tsamples.shift()\n\t\t}\n\t}\n\n\treturn {\n\t\tname: `recent${Math.round(windowMs / 1000)}sErrorRate`,\n\t\tweight,\n\t\trecordSuccess(): void {\n\t\t\tsamples.push({ t: now(), e: false })\n\t\t},\n\t\trecordError(): void {\n\t\t\tsamples.push({ t: now(), e: true })\n\t\t},\n\t\treset(): void {\n\t\t\tsamples.length = 0\n\t\t},\n\t\tread(): SignalReading {\n\t\t\tconst t = now()\n\t\t\tpruneExpired(t)\n\t\t\tif (samples.length === 0 || samples.length < minSamples) {\n\t\t\t\t// Insufficient traffic → don't penalise. Return rate=0 +\n\t\t\t\t// factor=1 (visible in JSON for ops triage).\n\t\t\t\treturn { value: 0, healthFactor: 1 }\n\t\t\t}\n\t\t\tlet errors = 0\n\t\t\tfor (const s of samples) {\n\t\t\t\tif (s.e) errors++\n\t\t\t}\n\t\t\tconst rate = errors / samples.length\n\n\t\t\tlet factor: number\n\t\t\tif (rate <= degradedRate) factor = 1\n\t\t\telse if (rate >= deadRate) factor = 0\n\t\t\telse {\n\t\t\t\tconst span = deadRate - degradedRate\n\t\t\t\tfactor = 1 - (rate - degradedRate) / span\n\t\t\t}\n\n\t\t\t// Round to 4 decimal places — wire JSON stays compact and the\n\t\t\t// extra precision is meaningless for ops use.\n\t\t\tconst valueRounded = Math.round(rate * 10_000) / 10_000\n\n\t\t\treturn {\n\t\t\t\tvalue: valueRounded,\n\t\t\t\thealthFactor: factor,\n\t\t\t}\n\t\t},\n\t}\n}\n","/**\n * `memoryPressureSignal` — RSS / cgroup memory.max ratio (ADR-111 §4.3).\n *\n * On Linux containers we read the cgroup v2 memory limit from\n * `/sys/fs/cgroup/memory.max`. Then `pressure = process.memoryUsage().rss /\n * limit`. ADR-111 §4.3 default thresholds:\n * degradedRatio = 0.85 (85 % → degraded)\n * deadRatio = 0.95 (95 % → dead)\n *\n * Mapping observed-pressure → healthFactor:\n *\n * healthFactor\n * ▲\n * 1.0 ┤━━━━━━━━━━━━━━━━━━┓\n * │ ┃\n * 0.0 ┤ ┗━━━━━━━━━\n * ┼──────────────────┼─────────► pressure (0..1)\n * 0 degradedRatio deadRatio\n *\n * Graceful fallback (the cardinal-rule deference):\n * - cgroup file missing → unknown=true (signal ignored)\n * - cgroup file has 'max' → unlimited container, ratio undefined → unknown\n * - file unreadable / parse → unknown=true\n *\n * `unknown=true` makes the scoring strategy ignore the signal — we never\n * pretend to know memory pressure on a host where we can't measure it.\n *\n * cgroup v1 (legacy) lives at `/sys/fs/cgroup/memory/memory.limit_in_bytes`\n * — supported via `cgroupV1Path` for operators on older kernels.\n */\n\nimport { readFileSync } from 'node:fs'\nimport type { SignalReading, SyncSignal } from '../types'\n\nexport interface MemoryPressureOptions {\n\t/** Pressure below this is fully healthy (factor=1). Default 0.85. */\n\treadonly degradedRatio?: number\n\t/** Pressure above this is fully dead (factor=0). Default 0.95. */\n\treadonly deadRatio?: number\n\t/**\n\t * Custom cgroup v2 memory limit path. Default `/sys/fs/cgroup/memory.max`.\n\t */\n\treadonly cgroupV2Path?: string\n\t/**\n\t * Optional cgroup v1 fallback path. Default\n\t * `/sys/fs/cgroup/memory/memory.limit_in_bytes`. Used when v2 path is\n\t * unreadable AND `cgroupV1Path` is set or v2 doesn't exist.\n\t */\n\treadonly cgroupV1Path?: string\n\t/** Weight in the weighted-product score. Default 0.2 (ADR-111 §4.3). */\n\treadonly weight?: number\n\t/**\n\t * Optional injected `process.memoryUsage` for tests.\n\t * Default: `process.memoryUsage`.\n\t */\n\treadonly memoryUsage?: () => { rss: number }\n\t/** Optional injected file reader for tests. */\n\treadonly readFile?: (path: string) => string\n}\n\n/**\n * cgroup v1 reports a sentinel value (`9223372036854771712` =\n * `Number.MAX_SAFE_INTEGER` neighbourhood, well > 2^53) when the limit is\n * unconstrained. cgroup v2 uses the literal string `max`. Both collapse\n * to \"unlimited → unknown\".\n */\nconst CGROUP_V1_UNLIMITED_FLOOR = 1e18\n\nfunction tryRead(reader: (p: string) => string, path: string): string | null {\n\ttry {\n\t\treturn reader(path)\n\t} catch {\n\t\treturn null\n\t}\n}\n\n/**\n * Read the container memory limit in bytes. Returns `null` if unknown\n * (file missing, unparseable, or unlimited).\n *\n * Exposed for direct tests of the cgroup parsing logic.\n */\nexport function readContainerMemoryLimit(\n\treader: (p: string) => string,\n\tv2Path: string,\n\tv1Path: string,\n): number | null {\n\t// 1. Try cgroup v2 first — it's the modern default (Talos / cluster runs\n\t// cgroupv2 hybrid; ADR-111 cluster spec).\n\tconst v2 = tryRead(reader, v2Path)\n\tif (v2 !== null) {\n\t\tconst trimmed = v2.trim()\n\t\tif (trimmed === 'max' || trimmed === '') return null\n\t\tconst n = Number.parseInt(trimmed, 10)\n\t\tif (!Number.isFinite(n) || n <= 0) return null\n\t\t// Some kernels report ridiculous \"max-ish\" sentinel values via v2.\n\t\tif (n > CGROUP_V1_UNLIMITED_FLOOR) return null\n\t\treturn n\n\t}\n\t// 2. Fall back to cgroup v1 if v2 is unreadable.\n\tconst v1 = tryRead(reader, v1Path)\n\tif (v1 === null) return null\n\tconst trimmed = v1.trim()\n\tconst n = Number.parseInt(trimmed, 10)\n\tif (!Number.isFinite(n) || n <= 0) return null\n\tif (n > CGROUP_V1_UNLIMITED_FLOOR) return null\n\treturn n\n}\n\nexport function memoryPressureSignal(opts: MemoryPressureOptions = {}): SyncSignal {\n\tconst degradedRatio = opts.degradedRatio ?? 0.85\n\tconst deadRatio = opts.deadRatio ?? 0.95\n\tconst v2Path = opts.cgroupV2Path ?? '/sys/fs/cgroup/memory.max'\n\tconst v1Path = opts.cgroupV1Path ?? '/sys/fs/cgroup/memory/memory.limit_in_bytes'\n\tconst weight = opts.weight ?? 0.2\n\tconst memoryUsage =\n\t\topts.memoryUsage ??\n\t\t((): { rss: number } => {\n\t\t\tconst m = process.memoryUsage()\n\t\t\treturn { rss: m.rss }\n\t\t})\n\tconst readFile = opts.readFile ?? ((p: string) => readFileSync(p, 'utf8'))\n\n\tif (degradedRatio <= 0 || degradedRatio >= 1) {\n\t\tthrow new Error(`memoryPressureSignal: degradedRatio must be in (0, 1), got ${degradedRatio}`)\n\t}\n\tif (deadRatio <= degradedRatio || deadRatio > 1) {\n\t\tthrow new Error(\n\t\t\t`memoryPressureSignal: deadRatio must be in (degradedRatio, 1], got ${deadRatio}`,\n\t\t)\n\t}\n\n\t// Cache the cgroup limit — kernel doesn't change it at runtime in\n\t// Kubernetes (would require a deployment edit + pod recreate). One\n\t// readSync per process boot is cheap; per poll tick is wasteful.\n\tlet cachedLimit: number | null | undefined\n\tfunction getLimit(): number | null {\n\t\tif (cachedLimit === undefined) {\n\t\t\tcachedLimit = readContainerMemoryLimit(readFile, v2Path, v1Path)\n\t\t}\n\t\treturn cachedLimit\n\t}\n\n\treturn {\n\t\tname: 'memoryPressure',\n\t\tweight,\n\t\tread(): SignalReading {\n\t\t\tconst limit = getLimit()\n\t\t\tif (limit === null) {\n\t\t\t\t// No container limit known — degrade gracefully to \"unknown\"\n\t\t\t\t// rather than report a bogus ratio against host RAM.\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tlet rss: number\n\t\t\ttry {\n\t\t\t\trss = memoryUsage().rss\n\t\t\t} catch {\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tif (!Number.isFinite(rss) || rss < 0) {\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tconst ratio = rss / limit\n\n\t\t\tlet factor: number\n\t\t\tif (ratio <= degradedRatio) factor = 1\n\t\t\telse if (ratio >= deadRatio) factor = 0\n\t\t\telse {\n\t\t\t\tconst span = deadRatio - degradedRatio\n\t\t\t\tfactor = 1 - (ratio - degradedRatio) / span\n\t\t\t}\n\n\t\t\t// 3-decimal precision keeps the wire JSON small.\n\t\t\tconst valueRounded = Math.round(ratio * 1000) / 1000\n\t\t\treturn { value: valueRounded, healthFactor: factor }\n\t\t},\n\t}\n}\n","/**\n * `queueDepthSignal` — backpressure indicator (ADR-111 §4.3).\n *\n * Generic signal: the app provides a `getter` that returns the current\n * length of whatever queue the operator wants probed (BullMQ, RabbitMQ,\n * in-memory work pool, …). The signal does NOT own the queue — the app\n * does. We just measure.\n *\n * Mapping observed-depth → healthFactor:\n *\n * healthFactor\n * ▲\n * 1.0 ┤━━━━━━━━━━━━━━━━━━┓\n * │ ┃ linear interpolation\n * 0.0 ┤ ┗━━━━━━━━━━━━\n * ┼──────────────────┼─────────► depth\n * 0 fullThreshold\n *\n * Below `fullThreshold` → factor 1. At `fullThreshold` → factor 0. Above\n * → factor 0. The implicit \"degraded\" zone is `[0.5 × fullThreshold,\n * fullThreshold]` — depth at half-full produces factor 0.5. Operators\n * tune `fullThreshold` to whatever their queue reasonably hits at peak\n * load; \"100% full\" means \"drain new traffic\", not \"kill the pod\" (the\n * three-tier gate at the sidecar handles the kill decision).\n *\n * `getter` errors are swallowed — a thrown getter produces `unknown=true`\n * (so the scoring strategy ignores this signal, instead of falsely\n * reporting score=0). The app's bug shouldn't masquerade as a sidecar\n * decision.\n */\n\nimport type { AsyncSignal, SignalReading } from '../types'\n\nexport interface QueueDepthOptions {\n\t/**\n\t * Sync or async getter the SDK calls every poll tick. Must return a\n\t * non-negative integer. Throws → reading marked `unknown=true`.\n\t */\n\treadonly getter: () => number | Promise<number>\n\t/**\n\t * Depth at which the queue is considered \"full\" (factor=0). Linear\n\t * interp from 0..fullThreshold. No default — operator-specific.\n\t */\n\treadonly fullThreshold: number\n\t/**\n\t * Optional below-which factor is always 1 (a \"soft floor\"). Default 0\n\t * — the linear interp starts at depth 0.\n\t */\n\treadonly healthyBelow?: number\n\t/** Weight in the weighted-product score. Default 0.2 (ADR-111 §4.3). */\n\treadonly weight?: number\n\t/** Custom signal name. Default `queueDepth`. */\n\treadonly name?: string\n}\n\nexport function queueDepthSignal(opts: QueueDepthOptions): AsyncSignal {\n\tif (typeof opts.getter !== 'function') {\n\t\tthrow new Error('queueDepthSignal: getter must be a function')\n\t}\n\tif (!Number.isFinite(opts.fullThreshold) || opts.fullThreshold <= 0) {\n\t\tthrow new Error(`queueDepthSignal: fullThreshold must be > 0, got ${opts.fullThreshold}`)\n\t}\n\tconst healthyBelow = opts.healthyBelow ?? 0\n\tif (!Number.isFinite(healthyBelow) || healthyBelow < 0 || healthyBelow >= opts.fullThreshold) {\n\t\tthrow new Error(\n\t\t\t`queueDepthSignal: healthyBelow must be in [0, fullThreshold), got ${healthyBelow}`,\n\t\t)\n\t}\n\tconst weight = opts.weight ?? 0.2\n\n\treturn {\n\t\tname: opts.name ?? 'queueDepth',\n\t\tweight,\n\t\tasync read(): Promise<SignalReading> {\n\t\t\tlet raw: unknown\n\t\t\ttry {\n\t\t\t\traw = await opts.getter()\n\t\t\t} catch {\n\t\t\t\t// Cardinal-rule deference: getter blew up → unknown.\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tif (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 0) {\n\t\t\t\treturn { value: 0, healthFactor: 1, unknown: true }\n\t\t\t}\n\t\t\tconst depth = raw\n\n\t\t\tlet factor: number\n\t\t\tif (depth <= healthyBelow) factor = 1\n\t\t\telse if (depth >= opts.fullThreshold) factor = 0\n\t\t\telse {\n\t\t\t\tconst span = opts.fullThreshold - healthyBelow\n\t\t\t\tfactor = 1 - (depth - healthyBelow) / span\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tvalue: depth,\n\t\t\t\thealthFactor: factor,\n\t\t\t}\n\t\t},\n\t}\n}\n","/**\n * `@sylphx/sdk/health` — Phase B multi-signal health score (ADR-111 §4).\n *\n * Apps register signals (event-loop lag, queue depth, error rate, memory\n * pressure, …); the SDK folds them into a continuous score in `[0, 1]`;\n * the **sidecar** maps the score to liveness / readiness / drain via the\n * three-tier gate. The SDK's responsibility ends at exposing the score.\n *\n * The wire format the sidecar parses (ADR-111 §3.2.4):\n *\n * ```json\n * {\n * \"score\": 0.92,\n * \"signals\": {\n * \"eventLoopLagMs\": 12,\n * \"queueDepth\": 3,\n * \"recent5sErrorRate\": 0.001,\n * \"memoryPressure\": 0.45\n * },\n * \"lastTickAt\": \"2026-05-03T12:34:56.789Z\"\n * }\n * ```\n *\n * Default signals (if `sylphxHealth()` called with no `signals`):\n * - `eventLoopLagSignal({ degradedMs: 5000, deadMs: 30000 })` (weight 1.0)\n *\n * Three-tier gate (decided by **sidecar**, not SDK; ADR-111 §4.4):\n *\n * | Score | Liveness | Readiness | Effect |\n * | -------------- | -------- | --------- | --------------------- |\n * | > 0.8 | 200 | 200 | Normal traffic |\n * | (0.5, 0.8] | 200 | 503 | Drain new, no kill |\n * | <= 0.5 | 503 | 503 | Kill after threshold |\n *\n * Apps don't need to think about probe semantics — that's the sidecar's\n * job. The app just exposes the score. See `apps/health-agent/` for the\n * sidecar implementation.\n *\n * @example Worked example — OpenClaw under PDF-extract load (ADR-111 §4.5):\n *\n * ```text\n * eventLoopLagMs = 6000 → factor 0.4\n * queueDepth = 12 → factor 1.0\n * errorRate = 0.002 → factor 1.0\n * memoryPressure = 0.55 → factor 1.0\n * score = 0.4^0.4 × 1.0^0.6 ≈ 0.69\n *\n * → falls in [0.5, 0.8] → sidecar drains traffic, doesn't kill.\n * Pod gets to finish PDF extraction.\n * ```\n */\n\nimport { evaluateEffect as buildEvaluateEffect } from './effects'\nimport { createNodeHandler, createWebHandler, type HealthEvaluator } from './handler'\nimport { defaultScoringStrategy } from './scoring'\nimport { eventLoopLagSignal } from './signals/event-loop-lag'\nimport type { HealthScore, Signal, SylphxHealthOptions } from './types'\nimport {\n\tstartUnixSocketServer,\n\ttype UnixSocketServerHandle,\n\ttype UnixSocketServerOptions,\n} from './unix-socket-server'\n\nexport {\n\tcreateNodeHandler,\n\tcreateWebHandler,\n\ttype HealthEvaluator,\n} from './handler'\nexport { defaultScoringStrategy, weightedProduct } from './scoring'\nexport type {\n\tErrorRateOptions,\n\tErrorRateSignalHandle,\n} from './signals/error-rate'\nexport { errorRateSignal } from './signals/error-rate'\nexport type { EventLoopLagOptions } from './signals/event-loop-lag'\nexport { eventLoopLagSignal } from './signals/event-loop-lag'\nexport type { MemoryPressureOptions } from './signals/memory-pressure'\nexport { memoryPressureSignal } from './signals/memory-pressure'\nexport type { QueueDepthOptions } from './signals/queue-depth'\nexport { queueDepthSignal } from './signals/queue-depth'\n// Re-exports — public surface.\nexport type {\n\tAsyncSignal,\n\tHealthScore,\n\tHealthSnapshot,\n\tScoringStrategy,\n\tSignal,\n\tSignalBase,\n\tSignalReading,\n\tSylphxHealthOptions,\n\tSyncSignal,\n} from './types'\nexport { HealthError } from './types'\nexport type {\n\tUnixSocketServerHandle,\n\tUnixSocketServerOptions,\n} from './unix-socket-server'\n\n/**\n * The handle returned by `sylphxHealth()`. Owns the registered signals,\n * exposes evaluation in both Promise + Effect form, and produces an HTTP\n * handler / Unix-socket server.\n *\n * Call `dispose()` during graceful shutdown to release per-signal\n * resources (e.g. the `monitorEventLoopDelay()` histogram).\n */\nexport interface SylphxHealth extends HealthEvaluator {\n\t/** All signals registered (read-only). */\n\treadonly signals: ReadonlyArray<Signal>\n\t/** Snapshot evaluation as a Promise. */\n\tevaluate(): Promise<HealthScore>\n\t/** Snapshot evaluation as an Effect (per Rule 21 / ADR-058 Amendment). */\n\treadonly evaluateEffect: ReturnType<typeof buildEvaluateEffect>\n\t/**\n\t * Web Fetch API HTTP handler — works under Hono, Bun.serve, Next.js\n\t * route.ts, itty-router, Hattip. Always returns 200 + JSON; the\n\t * sidecar applies the three-tier 200/503 gate.\n\t */\n\thandler(): (req?: Request) => Promise<Response>\n\t/** Node.js classic `(req, res)` handler — for Express / classic Fastify. */\n\tnodeHandler(): ReturnType<typeof createNodeHandler>\n\t/**\n\t * Bind a Bun Unix-domain socket and serve the same JSON. The sidecar\n\t * polls `/var/run/sylphx/health.sock` by default (ADR-111 §3.2.4).\n\t */\n\tserveUnixSocket(opts?: UnixSocketServerOptions): UnixSocketServerHandle\n\t/**\n\t * Tear down all registered signals. Idempotent. Call during graceful\n\t * shutdown to release histograms, file watchers, etc.\n\t */\n\tdispose(): void\n}\n\n/**\n * Build a `SylphxHealth` instance.\n *\n * @example Hono integration:\n * ```ts\n * import { Hono } from 'hono'\n * import {\n * sylphxHealth,\n * eventLoopLagSignal,\n * queueDepthSignal,\n * errorRateSignal,\n * memoryPressureSignal,\n * } from '@sylphx/sdk/health'\n *\n * const errors = errorRateSignal({ window: '5s', degradedRate: 0.05 })\n *\n * const health = sylphxHealth({\n * signals: [\n * eventLoopLagSignal({ degradedMs: 5000, deadMs: 30000 }),\n * queueDepthSignal({ getter: () => queue.size, fullThreshold: 1000 }),\n * errors,\n * memoryPressureSignal({ degradedRatio: 0.85 }),\n * ],\n * })\n *\n * const app = new Hono()\n * app.get('/healthz', health.handler())\n *\n * // Track requests for the error-rate signal\n * app.use(async (c, next) => {\n * try { await next(); errors.recordSuccess() }\n * catch (err) { errors.recordError(); throw err }\n * })\n *\n * // Or — primary transport for the sidecar:\n * health.serveUnixSocket() // → /var/run/sylphx/health.sock\n * ```\n *\n * @example Worked example — OpenClaw under PDF-extract load (ADR-111 §4.5):\n *\n * ```text\n * eventLoopLagMs = 6000 → factor 0.4\n * queueDepth = 12 → factor 1.0\n * errorRate = 0.002 → factor 1.0\n * memoryPressure = 0.55 → factor 1.0\n * score = 0.4^0.4 × 1.0^0.6 ≈ 0.69\n *\n * → falls in [0.5, 0.8] → sidecar drains traffic, doesn't kill.\n * Pod gets to finish PDF extraction.\n * ```\n */\nexport function sylphxHealth(opts: SylphxHealthOptions = {}): SylphxHealth {\n\t// ADR-111 §4.6: zero-config default — register a single\n\t// event-loop-lag signal so apps get a meaningful score without any\n\t// boilerplate. Once the app registers richer signals it overrides this.\n\tconst signals: Signal[] =\n\t\topts.signals && opts.signals.length > 0\n\t\t\t? [...opts.signals]\n\t\t\t: [eventLoopLagSignal({ degradedMs: 5000, deadMs: 30000, weight: 1 })]\n\n\tconst scoringStrategy = opts.scoringStrategy ?? defaultScoringStrategy()\n\tconst now = opts.now ?? ((): Date => new Date())\n\n\tlet disposed = false\n\n\tconst evaluate = async (): Promise<HealthScore> => {\n\t\tif (disposed) {\n\t\t\tthrow new Error('sylphxHealth: instance disposed')\n\t\t}\n\t\t// Read all signals in parallel — async-getter signals (queueDepth)\n\t\t// shouldn't serialise behind sync ones. `Promise.all` propagates a\n\t\t// thrown signal directly to the caller; signals are expected to\n\t\t// internally swallow errors and return `unknown=true` instead, so\n\t\t// we never hit this path in normal operation.\n\t\tconst readings = await Promise.all(\n\t\t\tsignals.map(async (signal) => ({\n\t\t\t\tsignal,\n\t\t\t\treading: await signal.read(),\n\t\t\t})),\n\t\t)\n\n\t\tconst score = scoringStrategy(readings)\n\n\t\tconst signalsMap: Record<string, number | string | boolean> = {}\n\t\tfor (const { signal, reading } of readings) {\n\t\t\tsignalsMap[signal.name] = reading.value\n\t\t}\n\n\t\treturn {\n\t\t\tscore,\n\t\t\tsignals: signalsMap,\n\t\t\tlastTickAt: now().toISOString(),\n\t\t}\n\t}\n\n\tconst evaluator: HealthEvaluator = { evaluate }\n\n\tconst evaluateEffect = buildEvaluateEffect(evaluate)\n\n\treturn {\n\t\tsignals,\n\t\tevaluate,\n\t\tevaluateEffect,\n\t\thandler(): (req?: Request) => Promise<Response> {\n\t\t\treturn createWebHandler(evaluator)\n\t\t},\n\t\tnodeHandler(): ReturnType<typeof createNodeHandler> {\n\t\t\treturn createNodeHandler(evaluator)\n\t\t},\n\t\tserveUnixSocket(unixOpts?: UnixSocketServerOptions): UnixSocketServerHandle {\n\t\t\treturn startUnixSocketServer(evaluator, unixOpts)\n\t\t},\n\t\tdispose(): void {\n\t\t\tif (disposed) return\n\t\t\tdisposed = true\n\t\t\tfor (const s of signals) {\n\t\t\t\ttry {\n\t\t\t\t\ts.dispose?.()\n\t\t\t\t} catch {\n\t\t\t\t\t// dispose must not throw — swallow to guarantee idempotency\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t}\n}\n"],"mappings":"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","names":["trimmed","n","evaluateEffect"]}