@neat.is/core 0.4.26-dev.20260702 → 0.4.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ZX7PCMGZ.js → chunk-A3322JYS.js} +57 -3
- package/dist/chunk-A3322JYS.js.map +1 -0
- package/dist/{chunk-XOOCA5T7.js → chunk-BIY46Q6U.js} +25 -1
- package/dist/chunk-BIY46Q6U.js.map +1 -0
- package/dist/{chunk-O25KZNZK.js → chunk-QM6BMPVJ.js} +1200 -280
- package/dist/chunk-QM6BMPVJ.js.map +1 -0
- package/dist/{chunk-GAFTW2OX.js → chunk-UV5WSM7M.js} +2 -2
- package/dist/{chunk-IABNGQT2.js → chunk-XV4D7A3Z.js} +117 -6
- package/dist/chunk-XV4D7A3Z.js.map +1 -0
- package/dist/cli.cjs +1565 -546
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +49 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1382 -326
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -1
- package/dist/index.d.ts +102 -1
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +1543 -395
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-QYHUJ4OY.js → otel-grpc-ET5Z6KI6.js} +3 -3
- package/dist/server.cjs +1206 -235
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +4 -4
- package/package.json +2 -2
- package/dist/chunk-IABNGQT2.js.map +0 -1
- package/dist/chunk-O25KZNZK.js.map +0 -1
- package/dist/chunk-XOOCA5T7.js.map +0 -1
- package/dist/chunk-ZX7PCMGZ.js.map +0 -1
- /package/dist/{chunk-GAFTW2OX.js.map → chunk-UV5WSM7M.js.map} +0 -0
- /package/dist/{otel-grpc-QYHUJ4OY.js.map → otel-grpc-ET5Z6KI6.js.map} +0 -0
package/dist/server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildSearchIndex
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-BIY46Q6U.js";
|
|
4
4
|
import {
|
|
5
5
|
DEFAULT_PROJECT,
|
|
6
6
|
Projects,
|
|
@@ -13,15 +13,15 @@ import {
|
|
|
13
13
|
pathsForProject,
|
|
14
14
|
startPersistLoop,
|
|
15
15
|
startStalenessLoop
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-QM6BMPVJ.js";
|
|
17
17
|
import {
|
|
18
18
|
startOtelGrpcReceiver
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-UV5WSM7M.js";
|
|
20
20
|
import {
|
|
21
21
|
assertBindAuthority,
|
|
22
22
|
buildOtelReceiver,
|
|
23
23
|
readAuthEnv
|
|
24
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-A3322JYS.js";
|
|
25
25
|
|
|
26
26
|
// src/server.ts
|
|
27
27
|
import path from "path";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@neat.is/core",
|
|
3
|
-
"version": "0.4.26
|
|
3
|
+
"version": "0.4.26",
|
|
4
4
|
"description": "NEAT graph engine: tree-sitter extraction, OTel ingest, REST API",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"homepage": "https://neat.is",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@fastify/cors": "^10.0.0",
|
|
52
52
|
"@grpc/grpc-js": "^1.14.3",
|
|
53
53
|
"@grpc/proto-loader": "^0.8.0",
|
|
54
|
-
"@neat.is/types": "^0.4.26
|
|
54
|
+
"@neat.is/types": "^0.4.26",
|
|
55
55
|
"chokidar": "^4.0.3",
|
|
56
56
|
"fastify": "^5.0.0",
|
|
57
57
|
"graphology": "^0.25.4",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/daemon.ts","../src/unrouted.ts"],"sourcesContent":["/**\n * Multi-project daemon (ADR-049).\n *\n * Single long-lived process watching every project in the machine-level registry.\n * Per-project graph isolation: each registered project owns its own\n * `MultiDirectedGraph` slot keyed by name (ADR-026), and a failure during\n * one project's bootstrap is logged + marked `broken` without taking down\n * the rest of the daemon.\n *\n * MVP scope (v0.2.5):\n * - Read registry; refuse to boot when it's missing.\n * - Write PID at `~/.neat/neatd.pid` for external supervisors.\n * - Per project: load any existing snapshot, run initial extraction,\n * start a per-project persist loop.\n * - SIGHUP triggers a reload — re-reads the registry, picks up new\n * projects, drops removed ones, leaves untouched ones in place.\n * - Provide `routeSpanToProject(serviceName, projects)` for OTel ingest\n * to dispatch by `service.name` across registered projects, falling\n * back to `default` for unknown services per ADR-033.\n *\n * Out of MVP scope (deferred):\n * - Live OTel listener wiring per project — daemon exposes the routing\n * primitive; the actual receiver attachment lands alongside v0.2.6.\n * - Policy reload on `policy.json` mtime — `startWatch` already does this\n * per-project; the daemon-level loop reuses that machinery in a follow-up.\n * - Auto-restart on crash. PID file is the supervisor handoff.\n */\n\nimport {\n promises as fs,\n watch,\n renameSync,\n unlinkSync,\n writeFileSync,\n type FSWatcher,\n} from 'node:fs'\nimport path from 'node:path'\nimport { createRequire } from 'node:module'\nimport type { FastifyInstance } from 'fastify'\nimport { DEFAULT_PROJECT, getGraph, resetGraph, type NeatGraph } from './graph.js'\nimport { extractFromDirectory } from './extract.js'\nimport { loadGraphFromDisk, saveGraphToDisk, startPersistLoop } from './persist.js'\nimport { Projects, pathsForProject, type ProjectPaths } from './projects.js'\nimport { buildApi } from './api.js'\nimport { buildOtelReceiver, listenSteppingOtlp } from './otel.js'\nimport { attachGraphToEventBus } from './events.js'\nimport { handleSpan, makeErrorSpanWriter } from './ingest.js'\nimport {\n listProjects,\n pruneRegistry,\n registryPath,\n setStatus,\n touchLastSeen,\n writeAtomically,\n} from './registry.js'\nimport { assertBindAuthority, readAuthEnv } from './auth.js'\nimport {\n appendUnroutedSpan,\n buildUnroutedSpanRecord,\n unroutedErrorsPath,\n} from './unrouted.js'\nimport { NodeType, type RegistryEntry, type ServiceNode } from '@neat.is/types'\n\n// ── Per-project daemon self-description (ADR-096 / project-daemon contract) ──\n//\n// A project's daemon owns one file — `<project>/neat-out/daemon.json` — that\n// records where it bound and what it is serving. This is the single source of\n// truth for \"where is this project's daemon,\" read by the instrumentation (to\n// resolve its OTLP endpoint), the MCP config, the dashboard, and `neat ps`.\n//\n// The shape is pinned: the orchestrator persists `ports` here on first spawn\n// and reuses them on restart (§3), and the generated otel-init reads\n// `ports.otlp` to build its exporter endpoint. Anything that drifts from this\n// shape breaks the OBSERVED layer silently, so the read/write helpers live in\n// one place and every producer/consumer goes through them.\n\nexport interface DaemonPorts {\n rest: number\n otlp: number\n web: number\n}\n\nexport interface DaemonRecord {\n project: string\n projectPath: string\n pid: number\n status: 'running' | 'stopped'\n ports: DaemonPorts\n startedAt: string\n neatVersion: string\n}\n\n// `<project>/neat-out/daemon.json` — the authoritative per-project record.\nexport function daemonJsonPath(scanPath: string): string {\n return path.join(scanPath, 'neat-out', 'daemon.json')\n}\n\n// Machine-wide discovery directory. Honors NEAT_HOME exactly as registry.ts /\n// neatHomeFor do so tests sandboxing under a temp home land here too.\nexport function daemonsDiscoveryDir(home?: string): string {\n const base = home && home.length > 0 ? home : neatHomeFromEnv()\n return path.join(base, 'daemons')\n}\n\n// `~/.neat/daemons/<project>.json` — a lock-free discovery copy (§6). Each\n// daemon owns only its own file; losing the directory costs `neat ps`\n// convenience, never correctness.\nexport function daemonDiscoveryPath(project: string, home?: string): string {\n return path.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`)\n}\n\n// Keep the discovery filename to a safe single path segment. Project names are\n// basenames in practice, but a name carrying a separator must never escape the\n// daemons/ directory.\nfunction sanitizeDiscoveryName(project: string): string {\n return project.replace(/[^A-Za-z0-9._-]/g, '_')\n}\n\nfunction neatHomeFromEnv(): string {\n const env = process.env.NEAT_HOME\n if (env && env.length > 0) return path.resolve(env)\n const home = process.env.HOME ?? process.env.USERPROFILE ?? ''\n return path.join(home, '.neat')\n}\n\n// Best-effort read of a project's daemon.json. Returns null when the file is\n// absent or malformed — callers treat that as \"no daemon recorded here\" rather\n// than failing, so a corrupt record never wedges spawn-vs-reuse.\nexport async function readDaemonRecord(scanPath: string): Promise<DaemonRecord | null> {\n try {\n const raw = await fs.readFile(daemonJsonPath(scanPath), 'utf8')\n const parsed = JSON.parse(raw) as Partial<DaemonRecord>\n if (\n typeof parsed.project === 'string' &&\n parsed.ports &&\n typeof parsed.ports.rest === 'number' &&\n typeof parsed.ports.otlp === 'number' &&\n typeof parsed.ports.web === 'number'\n ) {\n return parsed as DaemonRecord\n }\n return null\n } catch {\n return null\n }\n}\n\n// Resolve the running @neat.is/core version for the daemon.json stamp. Mirrors\n// neatd.ts#localVersion — NEAT_LOCAL_VERSION overrides for tests, else the\n// bundled package.json, else a safe sentinel.\nexport function resolveNeatVersion(): string {\n if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {\n return process.env.NEAT_LOCAL_VERSION\n }\n try {\n const req = createRequire(import.meta.url)\n const pkg = req('../package.json') as { version?: string }\n return typeof pkg.version === 'string' ? pkg.version : '0.0.0'\n } catch {\n return '0.0.0'\n }\n}\n\n// Write the authoritative record + the machine-wide discovery copy, both\n// atomically (tmp + rename, §2). Best-effort on the discovery copy: it is a\n// read-optimization, so a failure to write it is logged but never aborts the\n// daemon. The neat-out/ record is the one that matters and bubbles its error.\nexport async function writeDaemonRecord(record: DaemonRecord, home?: string): Promise<void> {\n const body = JSON.stringify(record, null, 2) + '\\n'\n await writeAtomically(daemonJsonPath(record.projectPath), body)\n try {\n await writeAtomically(daemonDiscoveryPath(record.project, home), body)\n } catch (err) {\n console.warn(\n `neatd: could not write discovery copy for \"${record.project}\" — ${(err as Error).message}`,\n )\n }\n}\n\n// Mark the record stopped (neat-out/) and clear the discovery copy on graceful\n// shutdown (§2/§6). The neat-out/ record is kept with status:\"stopped\" so a\n// later read can tell \"shut down cleanly\" from \"never ran\"; the discovery copy\n// is removed so `neat ps` stops listing a dead daemon. Both best-effort —\n// shutdown must not throw on a missing file.\nexport async function clearDaemonRecord(record: DaemonRecord, home?: string): Promise<void> {\n try {\n const stopped: DaemonRecord = { ...record, status: 'stopped' }\n await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + '\\n')\n } catch {\n // best-effort\n }\n try {\n await fs.unlink(daemonDiscoveryPath(record.project, home))\n } catch {\n // best-effort — already gone is fine.\n }\n}\n\n// Reconcile a daemon's self-description synchronously on an unsupervised exit\n// (project-daemon contract §2). The graceful `stop()` path already marks the\n// record stopped and clears the discovery copy; this is the backstop for a\n// crash or a fatal signal, where there's no chance to await async fs. A\n// process-exit handler runs synchronously, so we mark the neat-out/ record\n// `stopped` (tmp + renameSync keeps it atomic) and remove the discovery copy\n// with sync calls. Best-effort throughout: a missing or already-reconciled\n// file is fine, and a failure here must never throw out of an exit handler.\nexport function reconcileDaemonRecordSync(record: DaemonRecord, home?: string): void {\n try {\n const stopped: DaemonRecord = { ...record, status: 'stopped' }\n const target = daemonJsonPath(record.projectPath)\n const tmp = `${target}.${process.pid}.tmp`\n writeFileSync(tmp, JSON.stringify(stopped, null, 2) + '\\n')\n renameSync(tmp, target)\n } catch {\n // best-effort\n }\n try {\n unlinkSync(daemonDiscoveryPath(record.project, home))\n } catch {\n // best-effort — already gone is fine.\n }\n}\n\nexport interface DaemonOptions {\n // Defaults to `~/.neat/`. Honors NEAT_HOME the same way registry.ts does.\n // Tests override via NEAT_HOME and don't pass this directly.\n neatHome?: string\n // ADR-096 — when set, this daemon is scoped to exactly one project. It serves\n // only that project, mounts a bare `/v1/traces` route that assigns every\n // incoming span to it (no service.name routing), and writes its\n // `daemon.json` self-description on start. `projectPath` is the project root\n // (the directory whose `neat-out/` holds the record). Absent → the legacy\n // multi-project daemon behaviour.\n project?: string\n projectPath?: string\n // Dashboard/web port to record in daemon.json. The daemon doesn't bind this\n // itself (neatd spawns the web UI), but it owns the record, so it stamps the\n // allocated value the orchestrator passed through.\n webPort?: number\n // ADR-063 — bind targets. Defaults to PORT (8080) / OTEL_PORT (4318) env\n // vars, matching server.ts. Tests pass 0 to get ephemeral ports.\n restPort?: number\n otlpPort?: number\n // ADR-063 — bind host. Defaults to HOST env (0.0.0.0).\n host?: string\n // ADR-063 — opt out of binding entirely (e.g. integration tests that\n // exercise daemon slots without needing the listeners). Production\n // `neatd start` never sets this.\n bindListeners?: boolean\n}\n\nexport interface ProjectSlot {\n entry: RegistryEntry\n graph: NeatGraph\n outPath: string\n paths: ProjectPaths\n stopPersist: () => void\n // #475 — removes the event-bus listeners attachGraphToEventBus installed\n // on this slot's graph. No-op for broken slots. Must run wherever the slot\n // is torn down or replaced, or a reloaded slot's old graph keeps emitting.\n detachEvents: () => void\n status: 'active' | 'broken'\n errorReason?: string\n}\n\n// Best-effort slot teardown — stop the persist loop and detach the slot's\n// graph from the event bus (#475). Every path that drops or replaces a slot\n// (registry removal, daemon stop, bind-failure rollback, broken-slot\n// recovery) goes through here so no path leaks listeners.\nfunction teardownSlot(slot: ProjectSlot): void {\n try {\n slot.stopPersist()\n } catch {\n // best-effort\n }\n try {\n slot.detachEvents()\n } catch {\n // best-effort\n }\n}\n\n// Issue #340 — per-project bootstrap state surface. The REST listener\n// flips to live the moment `app.listen()` returns; per-project routes\n// branch on this rather than waiting for every registered project's\n// extractFromDirectory pass to finish.\nexport type BootstrapPhase = 'bootstrapping' | 'active' | 'broken'\n\nexport interface BootstrapTracker {\n status: (name: string) => BootstrapPhase | undefined\n list: () => Array<{ name: string; status: BootstrapPhase; elapsedMs: number }>\n}\n\nexport interface DaemonHandle {\n // The slots currently being managed, keyed by project name. Tests inspect\n // this to assert isolation properties.\n slots: Map<string, ProjectSlot>\n // Re-read the registry. New entries get bootstrapped, removed ones get\n // their persist loops stopped, existing ones stay running.\n reload: () => Promise<void>\n // Graceful shutdown — stop every project's persist loop and remove the\n // PID file.\n stop: () => Promise<void>\n // Path to the PID file the daemon owns. Useful for test assertions.\n pidPath: string\n // ADR-063 — addresses where consumers reach the daemon. Empty string when\n // bindListeners is false. REST is the Fastify app's listening address;\n // OTLP is the receiver's.\n restAddress: string\n otlpAddress: string\n // Issue #340 — per-project bootstrap status, surfaced for orchestrator\n // poll loops and tests.\n bootstrap: BootstrapTracker\n // Resolves when the daemon's initial bootstrap pass has settled. Tests\n // that probe project-scoped routes immediately after startDaemon await\n // this; production callers use /health.\n initialBootstrap: Promise<void>\n // ADR-096 — the per-project self-description this daemon wrote, or null in\n // the legacy multi-project mode. Tests assert the persisted ports + status.\n daemonRecord: DaemonRecord | null\n // The resolved NEAT_HOME this daemon discovers under. The entrypoint needs it\n // to clear the right discovery copy when it reconciles daemon.json on an\n // unsupervised exit (project-daemon contract §2).\n neatHome: string\n}\n\nfunction neatHomeFor(opts: DaemonOptions): string {\n if (opts.neatHome && opts.neatHome.length > 0) return path.resolve(opts.neatHome)\n const env = process.env.NEAT_HOME\n if (env && env.length > 0) return path.resolve(env)\n const home = process.env.HOME ?? process.env.USERPROFILE ?? ''\n return path.join(home, '.neat')\n}\n\n/**\n * Resolve which project's graph an OTel span belongs to. Looks up the\n * `service.name` against the registry and returns the matching project's\n * name, or `DEFAULT_PROJECT` for unknown services so the FrontierNode\n * auto-creation flow keeps working per ADR-033.\n *\n * Pure function. Daemon callers pass a snapshot of the registry to avoid\n * per-span fs reads.\n *\n * Matching order (ADR-072 — real-world `service.name` rarely equals project\n * name; monorepos publish per-package names like `brief-api` under a\n * project named `brief`):\n *\n * 1. Exact: `entry.name === serviceName`.\n * 2. Hyphen/underscore-separated prefix: `entry.name` is a leading token\n * of `serviceName` (`brief` matches `brief-api`, `brief_worker`).\n * Longest-match wins so `brief-api` beats `brief` when both are\n * registered.\n * 3. Containment as a separator-delimited token (`api` inside\n * `brief-api-staging`).\n *\n * Routing eligibility (ADR-071):\n * - `active` matches at every pass (the steady-state path).\n * - `broken` also matches — the daemon needs the span to reach the broken\n * slot so the ingest-time auto-recover path can attempt a bootstrap and\n * lift the project back to `active`. The router only chooses the target;\n * whether the span actually lands is the ingest handler's decision.\n * - `paused` is intentionally not routed; the operator paused it on\n * purpose, so the span falls through to the default-project flow.\n *\n * Falls back to `DEFAULT_PROJECT` when nothing matches.\n */\nexport function routeSpanToProject(\n serviceName: string | undefined,\n projects: ReadonlyArray<RegistryEntry>,\n): string {\n if (!serviceName) return DEFAULT_PROJECT\n // Pass 1 — exact match.\n for (const entry of projects) {\n if (entry.status === 'paused') continue\n if (entry.name === serviceName) return entry.name\n }\n // Pass 2 — hyphen/underscore-separated prefix. Longest project name wins\n // so a registered `brief-api` outranks a registered `brief` when the\n // span's service.name is `brief-api-staging`.\n const candidates: RegistryEntry[] = []\n for (const entry of projects) {\n if (entry.status === 'paused') continue\n if (isTokenPrefix(entry.name, serviceName)) candidates.push(entry)\n }\n if (candidates.length > 0) {\n candidates.sort((a, b) => b.name.length - a.name.length)\n return candidates[0]!.name\n }\n // Pass 3 — containment as a separator-delimited token. Last-resort match\n // for `api` inside `brief-api-staging` when only `api` is registered.\n for (const entry of projects) {\n if (entry.status === 'paused') continue\n if (isTokenContained(entry.name, serviceName)) return entry.name\n }\n return DEFAULT_PROJECT\n}\n\n// True when `prefix` matches the first hyphen/underscore-separated token(s)\n// of `full`. `brief` matches `brief-api`, `brief_worker`, but not `briefcase`.\nfunction isTokenPrefix(prefix: string, full: string): boolean {\n if (prefix.length >= full.length) return false\n if (!full.startsWith(prefix)) return false\n const sep = full.charAt(prefix.length)\n return sep === '-' || sep === '_'\n}\n\n// True when `needle` appears in `haystack` bordered by separators on both\n// sides (so it's a complete token, not a substring of a longer word).\nfunction isTokenContained(needle: string, haystack: string): boolean {\n if (!haystack.includes(needle)) return false\n const tokens = haystack.split(/[-_]/)\n return tokens.includes(needle)\n}\n\n// Does this span's `service.name` belong to the single project this daemon\n// hosts? Single-project mode (ADR-096) binds the bare `/v1/traces` route to one\n// project, but the OS-default OTLP endpoint (`localhost:4318`) is shared: a\n// sibling service from a *different* project that exports with default settings\n// lands here too. Merging its spans would mint that service's ServiceNode +\n// incidents into this project's graph — cross-project contamination. We scope\n// delivery to the project's owned services and quarantine the rest.\n//\n// A span is owned when:\n// - it carries no `service.name` (SDK misconfig in this project's own app;\n// handleSpan routes it to `service:unidentified`, refs #374), or\n// - its `service.name` matches the project name the same way the multi-\n// project router matches (exact / token-prefix / token-contained — covers\n// the monorepo case where `brief` owns `brief-api`, `brief-worker`), or\n// - a ServiceNode with that name already exists in the project's graph\n// (statically extracted, or observed-and-adopted on an earlier span).\n//\n// Everything else is foreign and gets quarantined to the unrouted ledger rather\n// than merged. The trade is deliberate: a brand-new service of this project that\n// NEAT can't statically read and whose name doesn't echo the project name has\n// its first spans quarantined until extraction registers it — a far smaller\n// failure than an entire sibling project bleeding into this graph.\nfunction serviceNameMatchesProject(serviceName: string, project: string): boolean {\n if (serviceName === project) return true\n if (isTokenPrefix(project, serviceName)) return true\n if (isTokenContained(project, serviceName)) return true\n return false\n}\n\nfunction spanBelongsToSingleProject(\n graph: NeatGraph,\n project: string,\n serviceName: string | undefined,\n): boolean {\n if (!serviceName) return true\n if (serviceNameMatchesProject(serviceName, project)) return true\n return graph.someNode(\n (_id, attrs) =>\n attrs.type === NodeType.ServiceNode &&\n (attrs as ServiceNode).name === serviceName,\n )\n}\n\nasync function bootstrapProject(entry: RegistryEntry): Promise<ProjectSlot> {\n const paths = pathsForProject(entry.name, path.join(entry.path, 'neat-out'))\n\n // Path missing on disk → mark broken and surface the reason. Daemon\n // continues with the rest of the registry.\n try {\n const stat = await fs.stat(entry.path)\n if (!stat.isDirectory()) {\n throw new Error(`registered path ${entry.path} is not a directory`)\n }\n } catch (err) {\n await setStatus(entry.name, 'broken').catch(() => {})\n return {\n entry,\n // Empty graph is fine — `slots` keeps the entry visible in `status`\n // output; nothing routes to it because it's not 'active'.\n graph: getGraph(`__broken__:${entry.name}`),\n outPath: '',\n paths,\n stopPersist: () => {},\n detachEvents: () => {},\n status: 'broken',\n errorReason: (err as Error).message,\n }\n }\n\n // Use the project name as the in-memory graph key. Any prior contents\n // are wiped because the daemon owns the slot for the lifetime of this\n // bootstrap (ADR-030 — mutation authority).\n resetGraph(entry.name)\n const graph = getGraph(entry.name)\n const outPath = paths.snapshotPath\n\n await loadGraphFromDisk(graph, outPath)\n // #475 — wire graph mutations into the event bus (ADR-051) before extract\n // begins so the initial pass also produces node/edge events, mirroring\n // startWatch. Without this the daemon's SSE stream carries heartbeats and\n // nothing else: handleSse subscribes to a bus no producer feeds, and the\n // dashboard only catches up on a manual refresh.\n const detachEvents = attachGraphToEventBus(graph, { project: entry.name })\n try {\n await extractFromDirectory(graph, entry.path)\n // The daemon owns shutdown, so the persist loop must not exit the process\n // on a signal — that would end us before `stop()` clears the daemon.json,\n // discovery copy, and pid file. `stop()` flushes this graph one last time\n // as it tears the slot down (see below).\n const stopPersist = startPersistLoop(graph, outPath, { exitOnSignal: false })\n await touchLastSeen(entry.name).catch(() => {})\n\n return {\n entry,\n graph,\n outPath,\n paths,\n stopPersist,\n detachEvents,\n status: 'active',\n }\n } catch (err) {\n // Bootstrap died after the attach — detach before surfacing so a failed\n // slot can't leave listeners behind on its orphaned graph.\n detachEvents()\n throw err\n }\n}\n\nfunction resolveRestPort(opts: DaemonOptions): number {\n if (typeof opts.restPort === 'number') return opts.restPort\n const env = process.env.PORT\n if (env && env.length > 0) {\n const n = Number.parseInt(env, 10)\n if (Number.isFinite(n)) return n\n }\n return 8080\n}\n\nfunction resolveOtlpPort(opts: DaemonOptions): number {\n if (typeof opts.otlpPort === 'number') return opts.otlpPort\n const env = process.env.OTEL_PORT\n if (env && env.length > 0) {\n const n = Number.parseInt(env, 10)\n if (Number.isFinite(n)) return n\n }\n return 4318\n}\n\n// The web/dashboard port the daemon records in daemon.json. The daemon never\n// binds it (neatd spawns the web child), but it owns the self-description, so\n// it stamps the resolved value. NEAT_WEB_PORT overrides the canonical 6328.\nfunction resolveWebPort(): number {\n const env = process.env.NEAT_WEB_PORT\n if (env && env.length > 0) {\n const n = Number.parseInt(env, 10)\n if (Number.isFinite(n)) return n\n }\n return 6328\n}\n\n// Read the real bound port off a Fastify listen address (`http://host:port`).\n// When the requested port was 0 the kernel chose one, and daemon.json must\n// record what the app should actually reach — not the 0 we asked for. Falls\n// back to the requested port if the address can't be parsed.\nexport function portFromListenAddress(address: string, fallback: number): number {\n try {\n const port = new URL(address).port\n const n = Number.parseInt(port, 10)\n if (Number.isFinite(n) && n > 0) return n\n } catch {\n // fall through\n }\n return fallback\n}\n\nexport function resolveHost(opts: DaemonOptions, authTokenSet: boolean): string {\n if (opts.host && opts.host.length > 0) return opts.host\n const env = process.env.HOST\n if (env && env.length > 0) return env\n // Issue #341 — loopback-only default when the operator hasn't set a token.\n // Public-bind on a clean install demanded one before binding could\n // succeed, so the npx-`neat .` first-touch path used to refuse to come up;\n // pinning to 127.0.0.1 lets that path bind cleanly. Anyone wanting a\n // public bind sets `NEAT_AUTH_TOKEN` (and `HOST=0.0.0.0` if they want it\n // spelled out). `assertBindAuthority` stays exactly as it is — the\n // contract is right; the default was wrong.\n if (!authTokenSet) return '127.0.0.1'\n return '0.0.0.0'\n}\n\nexport async function startDaemon(opts: DaemonOptions = {}): Promise<DaemonHandle> {\n const home = neatHomeFor(opts)\n const regPath = registryPath()\n\n // ADR-096 — single-project mode. The orchestrator spawns a daemon scoped to\n // one project, passing its name + root. In that mode the daemon serves only\n // that project: it bootstraps the one slot, mounts a bare `/v1/traces` route\n // that assigns every span to it, and writes its `daemon.json`\n // self-description. The legacy multi-project path (no opts.project) is left\n // intact so nothing on main breaks while Wave 2 retires the registry.\n //\n // The mode resolves from explicit opts first (the production path: neatd\n // reads NEAT_PROJECT/NEAT_PROJECT_PATH and passes them through), falling\n // back to the env directly so a bare `NEAT_PROJECT=… neatd start` works too.\n const projectArg =\n typeof opts.project === 'string' && opts.project.length > 0\n ? opts.project\n : process.env.NEAT_PROJECT && process.env.NEAT_PROJECT.length > 0\n ? process.env.NEAT_PROJECT\n : null\n const projectPathArg =\n opts.projectPath && opts.projectPath.length > 0\n ? opts.projectPath\n : process.env.NEAT_PROJECT_PATH && process.env.NEAT_PROJECT_PATH.length > 0\n ? process.env.NEAT_PROJECT_PATH\n : null\n const singleProject = projectArg\n const singleProjectPath =\n singleProject && projectPathArg ? path.resolve(projectPathArg) : null\n if (singleProject && !singleProjectPath) {\n throw new Error(\n `neatd: project \"${singleProject}\" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`,\n )\n }\n\n // Graceful degradation per ADR-049 #6: missing registry refuses to boot\n // with a clear error rather than silently coming up empty. A single-project\n // daemon takes its project from spawn args, not the registry, so it doesn't\n // gate on the registry file existing.\n if (!singleProject) {\n try {\n await fs.access(regPath)\n } catch {\n throw new Error(\n `neatd: registry not found at ${regPath}. Run \\`neat init <path>\\` to register a project before starting the daemon.`,\n )\n }\n }\n\n const pidPath = path.join(home, 'neatd.pid')\n await writeAtomically(pidPath, `${process.pid}\\n`)\n\n const slots = new Map<string, ProjectSlot>()\n // Projects registry mirrors slots for the REST listener (ADR-063). buildApi\n // reads from this; we keep it in sync as slots come and go.\n const registry = new Projects()\n // Issue #340 — per-project bootstrap status. Populated from the registry\n // before the listener binds so the REST handlers can return 503 instead of\n // 404 for projects still extracting.\n const bootstrapStatus = new Map<string, BootstrapPhase>()\n const bootstrapStartedAt = new Map<string, number>()\n\n // Rate-limit the dropped-span warning to one log line per project per\n // 60 seconds. OTel exporters retry on a tight cadence; without this we\n // flood the console with the same line per batch when a broken project\n // is sitting in the registry.\n const DROP_WARN_INTERVAL_MS = 60_000\n const lastDropWarnAt = new Map<string, number>()\n function warnDroppedSpan(project: string, reason: string): void {\n const now = Date.now()\n const prev = lastDropWarnAt.get(project) ?? 0\n if (now - prev < DROP_WARN_INTERVAL_MS) return\n lastDropWarnAt.set(project, now)\n console.warn(\n `[neatd] dropping span for project \"${project}\" — project status: broken (${reason}). Run \\`neatd reload\\` to retry bootstrap.`,\n )\n }\n\n // v0.4.1 / refs #339 — when a span's `service.name` doesn't match any\n // registered project AND no `default` project is registered, the span has\n // nowhere to land. We still return 200 on the receiver (OTel spec) but the\n // event lands in <NEAT_HOME>/errors.ndjson so the next operator can see\n // what happened instead of the daemon's stderr being the only signal.\n // Same rate limit as the broken-project warning, keyed by service.name.\n const unroutedPath = unroutedErrorsPath(home)\n const lastUnroutedWarnAt = new Map<string, number>()\n async function recordUnroutedSpan(\n serviceName: string | undefined,\n traceId: string | undefined,\n ): Promise<void> {\n const key = serviceName ?? '<missing>'\n const now = Date.now()\n try {\n await appendUnroutedSpan(home, buildUnroutedSpanRecord(serviceName, traceId, new Date(now)))\n } catch {\n // best-effort — failing to log shouldn't cascade into receiver failure.\n }\n const prev = lastUnroutedWarnAt.get(key) ?? 0\n if (now - prev < DROP_WARN_INTERVAL_MS) return\n lastUnroutedWarnAt.set(key, now)\n console.warn(\n `[neatd] dropping span — service.name \"${key}\" matches no registered project and no \\`default\\` project exists. See ${unroutedPath}.`,\n )\n }\n\n function upsertRegistryFromSlot(slot: ProjectSlot): void {\n if (slot.status !== 'active') return\n registry.set(slot.entry.name, {\n scanPath: slot.entry.path,\n paths: slot.paths,\n graph: slot.graph,\n })\n }\n\n // Attempt to bring a broken slot back online. Used both on SIGHUP reload\n // and inline on ingest when a span arrives for a broken project. Returns\n // the new slot status so callers can decide whether to deliver the span.\n async function tryRecoverSlot(entry: RegistryEntry): Promise<ProjectSlot> {\n try {\n const fresh = await bootstrapProject(entry)\n // The slot being replaced must release its graph's bus listeners\n // (#475) — a stale attach on the prior graph would double-emit.\n const prior = slots.get(entry.name)\n if (prior) teardownSlot(prior)\n slots.set(entry.name, fresh)\n upsertRegistryFromSlot(fresh)\n if (fresh.status === 'active') {\n await setStatus(entry.name, 'active').catch(() => {})\n console.log(\n `neatd: project \"${entry.name}\" recovered from broken — active`,\n )\n }\n return fresh\n } catch (err) {\n console.warn(\n `neatd: project \"${entry.name}\" still broken after recovery attempt — ${(err as Error).message}`,\n )\n // Leave the existing broken slot in place; nothing changed.\n return slots.get(entry.name)!\n }\n }\n\n async function bootstrapOne(entry: RegistryEntry): Promise<void> {\n bootstrapStatus.set(entry.name, 'bootstrapping')\n bootstrapStartedAt.set(entry.name, Date.now())\n try {\n const slot = await bootstrapProject(entry)\n // Same replacement rule as tryRecoverSlot (#475).\n const prior = slots.get(entry.name)\n if (prior) teardownSlot(prior)\n slots.set(entry.name, slot)\n upsertRegistryFromSlot(slot)\n bootstrapStatus.set(entry.name, slot.status === 'broken' ? 'broken' : 'active')\n if (slot.status === 'broken') {\n console.warn(`neatd: project \"${entry.name}\" broken — ${slot.errorReason}`)\n } else {\n console.log(`neatd: project \"${entry.name}\" active (${entry.path})`)\n }\n } catch (err) {\n bootstrapStatus.set(entry.name, 'broken')\n console.warn(\n `neatd: project \"${entry.name}\" failed to bootstrap — ${(err as Error).message}`,\n )\n await setStatus(entry.name, 'broken').catch(() => {})\n }\n }\n\n // The set of projects this daemon manages. Single-project mode (ADR-096)\n // takes its one project from spawn args and never reads the registry for the\n // project list; the legacy daemon enumerates every registered project.\n async function enumerateProjects(): Promise<RegistryEntry[]> {\n if (singleProject && singleProjectPath) {\n return [\n {\n name: singleProject,\n path: singleProjectPath,\n registeredAt: new Date().toISOString(),\n languages: [],\n status: 'active',\n },\n ]\n }\n return listProjects()\n }\n\n async function loadAll(): Promise<void> {\n // #463 — drop long-dead entries before bootstrapping the rest. An entry\n // whose path is gone (definite ENOENT) and that's been quiet past the TTL\n // gets removed instead of marked `broken` and logged forever. Conservative\n // by design: a transient stat error or a fresh ENOENT entry stays, and the\n // staleness TTL is the safety margin. Best-effort — a prune failure never\n // blocks the daemon from coming up. A single-project daemon owns no\n // registry coordination, so it skips the prune entirely.\n if (!singleProject) {\n try {\n const pruned = await pruneRegistry()\n for (const entry of pruned) {\n console.log(\n `neatd: pruned project \"${entry.name}\" — registered path ${entry.path} is gone`,\n )\n slots.delete(entry.name)\n bootstrapStatus.delete(entry.name)\n bootstrapStartedAt.delete(entry.name)\n }\n } catch (err) {\n console.warn(`neatd: registry prune skipped — ${(err as Error).message}`)\n }\n }\n\n const projects = await enumerateProjects()\n const seen = new Set<string>()\n const pending: Promise<void>[] = []\n for (const entry of projects) {\n seen.add(entry.name)\n const existing = slots.get(entry.name)\n if (existing) {\n if (existing.status === 'broken') {\n pending.push(tryRecoverSlot(entry).then(() => {}))\n }\n continue\n }\n pending.push(bootstrapOne(entry))\n }\n for (const [name, slot] of [...slots.entries()]) {\n if (seen.has(name)) continue\n teardownSlot(slot)\n slots.delete(name)\n bootstrapStatus.delete(name)\n bootstrapStartedAt.delete(name)\n console.log(`neatd: project \"${name}\" removed from registry — stopped`)\n }\n await Promise.allSettled(pending)\n }\n\n // Issue #340 — pre-populate bootstrap status from the registry so the REST\n // listener can answer 503 for projects whose slot hasn't loaded yet. Actual\n // bootstrap moves to the background after `listen()` returns.\n const initialEntries = await enumerateProjects().catch(() => [] as RegistryEntry[])\n for (const entry of initialEntries) {\n bootstrapStatus.set(entry.name, 'bootstrapping')\n bootstrapStartedAt.set(entry.name, Date.now())\n }\n\n // ADR-063 — bind the REST host and the OTLP HTTP receiver. One listener\n // each, multi-tenant by project name in the URL (REST) and by service.name\n // dispatch (OTLP). Failure on either listen aborts startDaemon with a\n // surfacing error rather than letting the supervisor sit half-up.\n const bind = opts.bindListeners !== false\n let restApp: FastifyInstance | null = null\n let otlpApp:\n | (FastifyInstance & { flushPending: () => Promise<void> })\n | null = null\n let restAddress = ''\n let otlpAddress = ''\n // ADR-096 — the self-description this daemon owns, filled once it binds in\n // single-project mode. Null in legacy multi-project mode and when listeners\n // are skipped (bindListeners:false).\n let daemonRecord: DaemonRecord | null = null\n\n if (bind) {\n // ADR-073 §3 — fail-loud before binding. Loopback-only without a token is\n // fine (laptop dev); a public bind without one is not. Resolved here\n // ahead of the host so the loopback-default branch (issue #341) reads\n // the same token state the bind-authority gate does.\n const auth = readAuthEnv()\n const host = resolveHost(opts, Boolean(auth.authToken))\n const restPort = resolveRestPort(opts)\n const otlpPort = resolveOtlpPort(opts)\n\n assertBindAuthority(host, auth.authToken)\n\n try {\n restApp = await buildApi({\n projects: registry,\n authToken: auth.authToken,\n trustProxy: auth.trustProxy,\n publicRead: auth.publicRead,\n bootstrap: {\n status: (name) => bootstrapStatus.get(name),\n list: () => {\n const now = Date.now()\n return [...bootstrapStatus.entries()].map(([name, status]) => ({\n name,\n status,\n elapsedMs: now - (bootstrapStartedAt.get(name) ?? now),\n }))\n },\n },\n // ADR-096 §4/§5/§7 — hand the daemon's identity to buildApi so the REST\n // surface reflects \"the daemon is the project\": `GET /projects` reports\n // only this project (the dashboard pins to it), and the daemon-wide\n // `/health` carries it at the top level for the spawn-reuse identity\n // check. Absent for the legacy multi-project daemon.\n singleProject:\n singleProject && singleProjectPath\n ? { name: singleProject, path: singleProjectPath }\n : undefined,\n })\n restAddress = await restApp.listen({ port: restPort, host })\n // Fastify reports a 0.0.0.0 bind back as http://127.0.0.1:port, so the\n // raw listen address hides a wildcard bind behind a loopback URL. Log the\n // host we actually asked for so the line matches what the port allocator\n // probed (the orchestrator threads this same host into its free check).\n console.log(\n `neatd: REST listening on http://${host}:${portFromListenAddress(restAddress, restPort)}`,\n )\n } catch (err) {\n // Roll back anything we started so far before surfacing the error.\n for (const slot of slots.values()) {\n teardownSlot(slot)\n }\n if (restApp) await restApp.close().catch(() => {})\n await fs.unlink(pidPath).catch(() => {})\n throw new Error(\n `neatd: failed to bind REST on port ${restPort} — ${(err as Error).message}`,\n )\n }\n\n // Resolve a span's target slot — running the broken-state recovery\n // when the routed slot is currently broken. Returns null when the span\n // can't be delivered after the recovery attempt; the caller drops with\n // a rate-limited warning. v0.4.1 / refs #339 — when nothing matches and\n // no default slot exists, the no-project-match event lands in\n // <NEAT_HOME>/errors.ndjson before we return null.\n //\n // ADR-096 single-project mode short-circuits all of that: the daemon hosts\n // exactly one project, so every span on the bare `/v1/traces` route is its\n // span. The 3-pass `routeSpanToProject` heuristic and the unrouted-span\n // drop are moot here — assigning by service.name could only ever mis-route\n // or drop a span the daemon definitionally owns, which is precisely the\n // silent-dark-OBSERVED failure §1 exists to kill. We assign directly,\n // recovering the slot if it's broken, and never write to errors.ndjson on\n // the no-match path.\n async function resolveTargetSlot(\n serviceName: string | undefined,\n traceId: string | undefined,\n ): Promise<ProjectSlot | null> {\n if (singleProject) {\n let slot = slots.get(singleProject)\n if (!slot) {\n // The sole slot hasn't bootstrapped yet (span arrived during the\n // initial extraction window). Build it on demand from spawn args so\n // the span isn't dropped — the OBSERVED layer must not go dark.\n slot = await tryRecoverSlot({\n name: singleProject,\n path: singleProjectPath!,\n registeredAt: new Date().toISOString(),\n languages: [],\n status: 'active',\n })\n } else if (slot.status === 'broken') {\n slot = await tryRecoverSlot(slot.entry)\n }\n if (!slot || slot.status !== 'active') {\n warnDroppedSpan(singleProject, slot?.errorReason ?? 'unknown')\n return null\n }\n // Scope to this project's owned services — quarantine a sibling\n // project's spans that reached our shared OTLP port instead of merging\n // them (cross-project contamination). The unrouted ledger records what\n // we dropped so it isn't silently dark.\n if (!spanBelongsToSingleProject(slot.graph, singleProject, serviceName)) {\n await recordUnroutedSpan(serviceName, traceId)\n return null\n }\n return slot\n }\n const liveEntries = await listProjects().catch(() => [])\n const target = routeSpanToProject(serviceName, liveEntries)\n let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT)\n if (!slot) {\n await recordUnroutedSpan(serviceName, traceId)\n return null\n }\n if (slot.status === 'broken') {\n const entry = liveEntries.find((e) => e.name === slot!.entry.name)\n if (entry) {\n slot = await tryRecoverSlot(entry)\n }\n if (slot.status !== 'active') {\n warnDroppedSpan(slot.entry.name, slot.errorReason ?? 'unknown')\n return null\n }\n }\n return slot.status === 'active' ? slot : null\n }\n\n // Resolve a project slot by its registered name — the path the\n // project-scoped OTLP route (issue #367) takes. URL-extracted project\n // names sidestep the service.name heuristic; we still want the broken\n // -slot recovery + unrouted-span logging so the route's failure modes\n // match the legacy path's.\n async function resolveSlotByName(\n project: string,\n serviceName: string | undefined,\n traceId: string | undefined,\n ): Promise<ProjectSlot | null> {\n const liveEntries = await listProjects().catch(() => [])\n let slot = slots.get(project)\n if (!slot) {\n await recordUnroutedSpan(serviceName, traceId)\n return null\n }\n if (slot.status === 'broken') {\n const entry = liveEntries.find((e) => e.name === slot!.entry.name)\n if (entry) {\n slot = await tryRecoverSlot(entry)\n }\n if (slot.status !== 'active') {\n warnDroppedSpan(slot.entry.name, slot.errorReason ?? 'unknown')\n return null\n }\n }\n return slot.status === 'active' ? slot : null\n }\n\n try {\n otlpApp = await buildOtelReceiver({\n authToken: auth.otelToken,\n trustProxy: auth.trustProxy,\n onSpan: async (span) => {\n // ADR-049 OTel routing — dispatch by service.name. Broken slots\n // get a single inline recovery attempt before the span is dropped\n // with a rate-limited log line. Unknown services route to\n // DEFAULT_PROJECT so the FrontierNode auto-creation flow keeps\n // working (ADR-033); when DEFAULT_PROJECT isn't registered either,\n // resolveTargetSlot writes a no-project-match event to\n // <NEAT_HOME>/errors.ndjson (refs #339).\n const slot = await resolveTargetSlot(span.service, span.traceId)\n if (!slot) return\n await handleSpan(\n {\n graph: slot.graph,\n errorsPath: slot.paths.errorsPath,\n scanPath: slot.entry.path,\n project: slot.entry.name,\n // Receiver already wrote the error event synchronously below.\n writeErrorEventInline: false,\n },\n span,\n )\n },\n onErrorSpanSync: async (span) => {\n const slot = await resolveTargetSlot(span.service, span.traceId)\n if (!slot) return\n await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span)\n },\n // Project-scoped route (issue #367) — the URL already named the\n // project. Resolution is a direct slot lookup; service.name resolves\n // the ServiceNode inside the slot's graph instead of which project\n // owns the span.\n onProjectSpan: async (project, span) => {\n const slot = await resolveSlotByName(project, span.service, span.traceId)\n if (!slot) return\n await handleSpan(\n {\n graph: slot.graph,\n errorsPath: slot.paths.errorsPath,\n scanPath: slot.entry.path,\n project: slot.entry.name,\n writeErrorEventInline: false,\n },\n span,\n )\n },\n onProjectErrorSpanSync: async (project, span) => {\n const slot = await resolveSlotByName(project, span.service, span.traceId)\n if (!slot) return\n await makeErrorSpanWriter(slot.paths.errorsPath, slot.graph, slot.entry.path)(span)\n },\n })\n // A held OTLP port steps to the next free one rather than crashing the\n // daemon (daemon.md §Binding). The recorded daemon.json port below reads\n // back from otlpAddress, so a stepped port is what otel-init resolves.\n otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host)\n console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`)\n } catch (err) {\n for (const slot of slots.values()) {\n teardownSlot(slot)\n }\n if (restApp) await restApp.close().catch(() => {})\n if (otlpApp) await otlpApp.close().catch(() => {})\n await fs.unlink(pidPath).catch(() => {})\n throw new Error(\n `neatd: failed to bind OTLP on port ${otlpPort} — ${(err as Error).message}`,\n )\n }\n\n // ADR-096 §2 — write the self-description now that both listeners are up\n // and we know the real bound ports. Reading them back from the listen\n // addresses (rather than the requested ports) is what makes ephemeral-port\n // tests and the orchestrator's allocation agree: when the requested port\n // was 0, the kernel chose one, and the generated otel-init must read THAT.\n if (singleProject && singleProjectPath) {\n const ports: DaemonPorts = {\n rest: portFromListenAddress(restAddress, restPort),\n otlp: portFromListenAddress(otlpAddress, otlpPort),\n // The daemon doesn't bind the web port itself (neatd spawns the web\n // child); it records the allocated value passed through so the\n // dashboard and `neat ps` agree on where to look.\n web: typeof opts.webPort === 'number' ? opts.webPort : resolveWebPort(),\n }\n daemonRecord = {\n project: singleProject,\n projectPath: singleProjectPath,\n pid: process.pid,\n status: 'running',\n ports,\n startedAt: new Date().toISOString(),\n neatVersion: resolveNeatVersion(),\n }\n try {\n await writeDaemonRecord(daemonRecord, home)\n console.log(\n `neatd: project \"${singleProject}\" → REST ${ports.rest} / OTLP ${ports.otlp} / web ${ports.web} (daemon.json written)`,\n )\n } catch (err) {\n // The neat-out/ record is load-bearing — without it the instrumented\n // app can't resolve its OTLP endpoint and the OBSERVED layer goes\n // dark. Fail loud, rolling back the listeners + pid like the bind\n // failures above.\n for (const slot of slots.values()) teardownSlot(slot)\n if (restApp) await restApp.close().catch(() => {})\n if (otlpApp) await otlpApp.close().catch(() => {})\n await fs.unlink(pidPath).catch(() => {})\n throw new Error(\n `neatd: failed to write daemon.json for \"${singleProject}\" — ${(err as Error).message}`,\n )\n }\n }\n }\n\n // Issue #340 — listeners are live; kick off per-project bootstrap in the\n // background. Polled callers watch /health for transitions.\n const initialBootstrap = loadAll().catch((err) => {\n console.warn(`neatd: initial bootstrap pass failed — ${(err as Error).message}`)\n })\n\n let reloading: Promise<void> | null = initialBootstrap\n const reload = async (): Promise<void> => {\n if (reloading) return reloading\n reloading = (async () => {\n try {\n await loadAll()\n } finally {\n reloading = null\n }\n })()\n return reloading\n }\n void initialBootstrap.finally(() => {\n if (reloading === initialBootstrap) reloading = null\n })\n\n const tracker: BootstrapTracker = {\n status: (name) => bootstrapStatus.get(name),\n list: () => {\n const now = Date.now()\n return [...bootstrapStatus.entries()].map(([name, status]) => ({\n name,\n status,\n elapsedMs: now - (bootstrapStartedAt.get(name) ?? now),\n }))\n },\n }\n\n // SIGHUP — external \"reload your config\" signal. ADR-049 #2.\n const sighupHandler = (): void => {\n void reload().catch((err) => {\n console.warn(`neatd: SIGHUP reload failed — ${(err as Error).message}`)\n })\n }\n process.on('SIGHUP', sighupHandler)\n\n // Issue #382 — registry watcher. The orchestrator writes new projects to\n // the registry file while the daemon is already running; without an explicit\n // `neatd reload`, the slot map stays stale and every span for the freshly-\n // registered project gets rejected as no-project-match. Watching the\n // registry's directory (more robust against the tmp+rename atomic-write\n // pattern from ADR-048 than file-path watches on some platforms) and\n // filtering by basename catches every change. Debounce collapses the 2-3\n // events the rename pattern fires per write into a single reload.\n const REGISTRY_RELOAD_DEBOUNCE_MS = 500\n let registryWatcher: FSWatcher | null = null\n let reloadTimer: NodeJS.Timeout | null = null\n // A single-project daemon takes its one project from spawn args and never\n // reads the machine registry for its project list, so there's nothing for\n // the registry watcher to react to — skip it (ADR-096 §6: no machine-wide\n // coordination surface).\n if (!singleProject) try {\n const regDir = path.dirname(regPath)\n const regBase = path.basename(regPath)\n registryWatcher = watch(regDir, (_eventType, filename) => {\n // filename can be null on some platforms — fall back to firing every\n // event, the debounce + reload's idempotency cover any over-fire.\n if (filename !== null && filename !== regBase) return\n if (reloadTimer) clearTimeout(reloadTimer)\n reloadTimer = setTimeout(() => {\n reloadTimer = null\n void reload().catch((err) => {\n console.warn(\n `neatd: registry-watch reload failed — ${(err as Error).message}`,\n )\n })\n }, REGISTRY_RELOAD_DEBOUNCE_MS)\n })\n } catch (err) {\n // Watching the registry is a best-effort optimisation over SIGHUP — if\n // the kernel refuses (e.g. inotify quota exhausted) we surface the\n // failure but let the daemon keep running.\n console.warn(\n `neatd: failed to watch registry at ${regPath} — ${(err as Error).message}. ` +\n `Run \\`neatd reload\\` (or send SIGHUP) after registering new projects.`,\n )\n }\n\n let stopped = false\n const stop = async (): Promise<void> => {\n if (stopped) return\n stopped = true\n process.off('SIGHUP', sighupHandler)\n if (reloadTimer) {\n clearTimeout(reloadTimer)\n reloadTimer = null\n }\n if (registryWatcher) {\n try {\n registryWatcher.close()\n } catch {\n // best-effort\n }\n registryWatcher = null\n }\n if (otlpApp) await otlpApp.close().catch(() => {})\n if (restApp) await restApp.close().catch(() => {})\n // Listeners are down, so the graph is now at its final state. Flush each\n // active slot once before tearing its persist loop down — the loops run\n // with `exitOnSignal: false`, so this is where the shutdown save lives now.\n for (const slot of slots.values()) {\n if (slot.status === 'active' && slot.outPath) {\n await saveGraphToDisk(slot.graph, slot.outPath).catch(() => {})\n }\n }\n for (const slot of slots.values()) {\n teardownSlot(slot)\n }\n // ADR-096 §2/§6 — mark the neat-out/ record stopped and remove the\n // machine-wide discovery copy so `neat ps` stops listing a dead daemon.\n if (daemonRecord) {\n await clearDaemonRecord(daemonRecord, home)\n }\n await fs.unlink(pidPath).catch(() => {})\n }\n\n return {\n slots,\n reload,\n stop,\n pidPath,\n restAddress,\n otlpAddress,\n bootstrap: tracker,\n initialBootstrap,\n daemonRecord,\n neatHome: home,\n }\n}\n","/**\n * Unrouted-span logging (v0.4.1 — refs #339).\n *\n * When the daemon's routing layer can't deliver a span — service.name\n * matches no registered project AND no `default` project exists — we still\n * return 200 on the receiver (OTLP spec), but the dropped event lands here\n * so the next operator can see what happened. The log lives at\n * `<NEAT_HOME>/errors.ndjson` because the unrouted span doesn't belong to\n * any project's neat-out directory.\n *\n * Owner: this module. Daemon imports the appender + warner; no other code\n * writes to the no-project-match log path. Keeping the writes here keeps\n * daemon.ts free of direct `fs.appendFile` calls (asserted by the daemon\n * contract test).\n */\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\n\nexport interface UnroutedSpanRecord {\n timestamp: string\n reason: 'no-project-match'\n service_name: string | null\n traceId: string | null\n}\n\nexport function buildUnroutedSpanRecord(\n serviceName: string | undefined,\n traceId: string | undefined,\n now: Date = new Date(),\n): UnroutedSpanRecord {\n return {\n timestamp: now.toISOString(),\n reason: 'no-project-match',\n service_name: serviceName ?? null,\n traceId: traceId ?? null,\n }\n}\n\nexport async function appendUnroutedSpan(\n neatHome: string,\n record: UnroutedSpanRecord,\n): Promise<void> {\n const target = path.join(neatHome, 'errors.ndjson')\n await fs.mkdir(neatHome, { recursive: true })\n await fs.appendFile(target, JSON.stringify(record) + '\\n', 'utf8')\n}\n\nexport function unroutedErrorsPath(neatHome: string): string {\n return path.join(neatHome, 'errors.ndjson')\n}\n"],"mappings":"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","names":["fs","path","path","fs","slot"]}
|