@neat.is/core 0.4.26-dev.20260703 → 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-2LNICOVU.js → chunk-A3322JYS.js} +29 -2
- package/dist/chunk-A3322JYS.js.map +1 -0
- package/dist/{chunk-4OR4RQEO.js → chunk-BIY46Q6U.js} +13 -1
- package/dist/chunk-BIY46Q6U.js.map +1 -0
- package/dist/{chunk-WZYH5DVG.js → chunk-QM6BMPVJ.js} +419 -218
- package/dist/chunk-QM6BMPVJ.js.map +1 -0
- package/dist/{chunk-C5NCCKPZ.js → chunk-UV5WSM7M.js} +2 -2
- package/dist/{chunk-S7TDPQDD.js → chunk-XV4D7A3Z.js} +105 -6
- package/dist/chunk-XV4D7A3Z.js.map +1 -0
- package/dist/cli.cjs +732 -456
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +49 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +587 -273
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -1
- package/dist/index.d.ts +97 -1
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +600 -286
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-LFYZDSK3.js → otel-grpc-ET5Z6KI6.js} +3 -3
- package/dist/server.cjs +410 -182
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +4 -4
- package/package.json +2 -2
- package/dist/chunk-2LNICOVU.js.map +0 -1
- package/dist/chunk-4OR4RQEO.js.map +0 -1
- package/dist/chunk-S7TDPQDD.js.map +0 -1
- package/dist/chunk-WZYH5DVG.js.map +0 -1
- /package/dist/{chunk-C5NCCKPZ.js.map → chunk-UV5WSM7M.js.map} +0 -0
- /package/dist/{otel-grpc-LFYZDSK3.js.map → otel-grpc-ET5Z6KI6.js.map} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
parseOtlpRequest
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-A3322JYS.js";
|
|
4
4
|
|
|
5
5
|
// src/otel-grpc.ts
|
|
6
6
|
import { fileURLToPath } from "url";
|
|
@@ -138,4 +138,4 @@ export {
|
|
|
138
138
|
reshapeGrpcRequest,
|
|
139
139
|
startOtelGrpcReceiver
|
|
140
140
|
};
|
|
141
|
-
//# sourceMappingURL=chunk-
|
|
141
|
+
//# sourceMappingURL=chunk-UV5WSM7M.js.map
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
Projects,
|
|
4
4
|
attachGraphToEventBus,
|
|
5
5
|
buildApi,
|
|
6
|
+
ensureObservedFileNode,
|
|
7
|
+
ensureServiceNode,
|
|
6
8
|
extractFromDirectory,
|
|
7
9
|
getGraph,
|
|
8
10
|
handleSpan,
|
|
@@ -11,6 +13,7 @@ import {
|
|
|
11
13
|
makeErrorSpanWriter,
|
|
12
14
|
pathsForProject,
|
|
13
15
|
pruneRegistry,
|
|
16
|
+
reconcileObservedRelPath,
|
|
14
17
|
registryPath,
|
|
15
18
|
resetGraph,
|
|
16
19
|
saveGraphToDisk,
|
|
@@ -18,14 +21,15 @@ import {
|
|
|
18
21
|
startPersistLoop,
|
|
19
22
|
startStalenessLoop,
|
|
20
23
|
touchLastSeen,
|
|
24
|
+
upsertObservedEdge,
|
|
21
25
|
writeAtomically
|
|
22
|
-
} from "./chunk-
|
|
26
|
+
} from "./chunk-QM6BMPVJ.js";
|
|
23
27
|
import {
|
|
24
28
|
assertBindAuthority,
|
|
25
29
|
buildOtelReceiver,
|
|
26
30
|
listenSteppingOtlp,
|
|
27
31
|
readAuthEnv
|
|
28
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-A3322JYS.js";
|
|
29
33
|
|
|
30
34
|
// src/daemon.ts
|
|
31
35
|
import {
|
|
@@ -38,6 +42,82 @@ import {
|
|
|
38
42
|
import path2 from "path";
|
|
39
43
|
import { createRequire } from "module";
|
|
40
44
|
|
|
45
|
+
// src/connectors/index.ts
|
|
46
|
+
var NO_ENV = "unknown";
|
|
47
|
+
async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
|
|
48
|
+
const signals = await connector.poll(ctx);
|
|
49
|
+
let edgesCreated = 0;
|
|
50
|
+
let edgesUpdated = 0;
|
|
51
|
+
let unresolved = 0;
|
|
52
|
+
for (const signal of signals) {
|
|
53
|
+
const resolved = resolveTarget(signal, ctx);
|
|
54
|
+
if (!resolved) {
|
|
55
|
+
unresolved++;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const serviceNodeId = ensureServiceNode(graph, resolved.serviceName, NO_ENV);
|
|
59
|
+
const callSite = signal.callSite ? { relPath: signal.callSite.file, line: signal.callSite.line } : void 0;
|
|
60
|
+
const sourceId = callSite ? ensureObservedFileNode(graph, resolved.serviceName, serviceNodeId, callSite) : serviceNodeId;
|
|
61
|
+
const evidence = callSite ? {
|
|
62
|
+
file: reconcileObservedRelPath(graph, resolved.serviceName, callSite.relPath),
|
|
63
|
+
line: callSite.line
|
|
64
|
+
} : void 0;
|
|
65
|
+
const calls = Math.trunc(signal.callCount);
|
|
66
|
+
if (calls < 1) continue;
|
|
67
|
+
const errors = Math.min(Math.max(Math.trunc(signal.errorCount), 0), calls);
|
|
68
|
+
let created = false;
|
|
69
|
+
let ok = true;
|
|
70
|
+
for (let i = 0; i < calls; i++) {
|
|
71
|
+
const result = upsertObservedEdge(
|
|
72
|
+
graph,
|
|
73
|
+
resolved.edgeType,
|
|
74
|
+
sourceId,
|
|
75
|
+
resolved.targetNodeId,
|
|
76
|
+
signal.lastObservedIso,
|
|
77
|
+
i < errors,
|
|
78
|
+
evidence
|
|
79
|
+
);
|
|
80
|
+
if (!result) {
|
|
81
|
+
ok = false;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
if (i === 0) created = result.created;
|
|
85
|
+
}
|
|
86
|
+
if (!ok) {
|
|
87
|
+
unresolved++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (created) edgesCreated++;
|
|
91
|
+
else edgesUpdated++;
|
|
92
|
+
}
|
|
93
|
+
return { signalCount: signals.length, edgesCreated, edgesUpdated, unresolved };
|
|
94
|
+
}
|
|
95
|
+
var DEFAULT_POLL_INTERVAL_MS = 6e4;
|
|
96
|
+
function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options = {}) {
|
|
97
|
+
let stopped = false;
|
|
98
|
+
let since = ctx.since;
|
|
99
|
+
const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
100
|
+
const onError = options.onError ?? ((err) => console.error(`[neatd] connector poll failed (${connector.provider})`, err));
|
|
101
|
+
const tick = () => {
|
|
102
|
+
if (stopped) return;
|
|
103
|
+
void (async () => {
|
|
104
|
+
const tickStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
105
|
+
try {
|
|
106
|
+
await runConnectorPoll(connector, { ...ctx, since }, graph, resolveTarget);
|
|
107
|
+
since = tickStartedAt;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
onError(err);
|
|
110
|
+
}
|
|
111
|
+
})();
|
|
112
|
+
};
|
|
113
|
+
const interval = setInterval(tick, intervalMs);
|
|
114
|
+
if (typeof interval.unref === "function") interval.unref();
|
|
115
|
+
return () => {
|
|
116
|
+
stopped = true;
|
|
117
|
+
clearInterval(interval);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
41
121
|
// src/unrouted.ts
|
|
42
122
|
import { promises as fs } from "fs";
|
|
43
123
|
import path from "path";
|
|
@@ -148,6 +228,10 @@ function teardownSlot(slot) {
|
|
|
148
228
|
slot.stopStaleness();
|
|
149
229
|
} catch {
|
|
150
230
|
}
|
|
231
|
+
try {
|
|
232
|
+
slot.stopConnectors();
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
151
235
|
try {
|
|
152
236
|
slot.detachEvents();
|
|
153
237
|
} catch {
|
|
@@ -205,7 +289,7 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
|
|
|
205
289
|
(_id, attrs) => attrs.type === NodeType.ServiceNode && attrs.name === serviceName
|
|
206
290
|
);
|
|
207
291
|
}
|
|
208
|
-
async function bootstrapProject(entry) {
|
|
292
|
+
async function bootstrapProject(entry, connectors = []) {
|
|
209
293
|
const paths = pathsForProject(entry.name, path2.join(entry.path, "neat-out"));
|
|
210
294
|
try {
|
|
211
295
|
const stat = await fs2.stat(entry.path);
|
|
@@ -226,6 +310,8 @@ async function bootstrapProject(entry) {
|
|
|
226
310
|
},
|
|
227
311
|
stopStaleness: () => {
|
|
228
312
|
},
|
|
313
|
+
stopConnectors: () => {
|
|
314
|
+
},
|
|
229
315
|
detachEvents: () => {
|
|
230
316
|
},
|
|
231
317
|
status: "broken",
|
|
@@ -244,6 +330,18 @@ async function bootstrapProject(entry) {
|
|
|
244
330
|
staleEventsPath: paths.staleEventsPath,
|
|
245
331
|
project: entry.name
|
|
246
332
|
});
|
|
333
|
+
const stopFns = connectors.map(
|
|
334
|
+
(registration) => startConnectorPollLoop(
|
|
335
|
+
registration.connector,
|
|
336
|
+
{ projectDir: entry.path, credentials: registration.credentials },
|
|
337
|
+
graph,
|
|
338
|
+
registration.resolveTarget,
|
|
339
|
+
{ intervalMs: registration.intervalMs }
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
const stopConnectors = () => {
|
|
343
|
+
for (const stop of stopFns) stop();
|
|
344
|
+
};
|
|
247
345
|
await touchLastSeen(entry.name).catch(() => {
|
|
248
346
|
});
|
|
249
347
|
return {
|
|
@@ -253,6 +351,7 @@ async function bootstrapProject(entry) {
|
|
|
253
351
|
paths,
|
|
254
352
|
stopPersist,
|
|
255
353
|
stopStaleness,
|
|
354
|
+
stopConnectors,
|
|
256
355
|
detachEvents,
|
|
257
356
|
status: "active"
|
|
258
357
|
};
|
|
@@ -368,7 +467,7 @@ async function startDaemon(opts = {}) {
|
|
|
368
467
|
}
|
|
369
468
|
async function tryRecoverSlot(entry) {
|
|
370
469
|
try {
|
|
371
|
-
const fresh = await bootstrapProject(entry);
|
|
470
|
+
const fresh = await bootstrapProject(entry, opts.connectors ?? []);
|
|
372
471
|
const prior = slots.get(entry.name);
|
|
373
472
|
if (prior) teardownSlot(prior);
|
|
374
473
|
slots.set(entry.name, fresh);
|
|
@@ -392,7 +491,7 @@ async function startDaemon(opts = {}) {
|
|
|
392
491
|
bootstrapStatus.set(entry.name, "bootstrapping");
|
|
393
492
|
bootstrapStartedAt.set(entry.name, Date.now());
|
|
394
493
|
try {
|
|
395
|
-
const slot = await bootstrapProject(entry);
|
|
494
|
+
const slot = await bootstrapProject(entry, opts.connectors ?? []);
|
|
396
495
|
const prior = slots.get(entry.name);
|
|
397
496
|
if (prior) teardownSlot(prior);
|
|
398
497
|
slots.set(entry.name, slot);
|
|
@@ -805,4 +904,4 @@ export {
|
|
|
805
904
|
resolveHost,
|
|
806
905
|
startDaemon
|
|
807
906
|
};
|
|
808
|
-
//# sourceMappingURL=chunk-
|
|
907
|
+
//# sourceMappingURL=chunk-XV4D7A3Z.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/daemon.ts","../src/connectors/index.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, startStalenessLoop } from './ingest.js'\nimport { startConnectorPollLoop, type ConnectorRegistration } from './connectors/index.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 // Connectors plane (docs/contracts/connectors.md, ADR-124) — pull-based\n // OBSERVED connectors polled on an interval alongside every project this\n // daemon bootstraps, the same way every project gets the staleness loop.\n // Applied identically to every project slot; there is no per-project\n // config-loading here yet (that's provider-specific, later work) — a\n // caller wanting project-scoped connectors runs a separate `startDaemon`\n // per project (the single-project ADR-096 mode is the common case).\n connectors?: ConnectorRegistration[]\n}\n\nexport interface ProjectSlot {\n entry: RegistryEntry\n graph: NeatGraph\n outPath: string\n paths: ProjectPaths\n stopPersist: () => void\n // Stops the OBSERVED→STALE clock-decay loop for this slot. Runs on the same\n // 60s cadence as `neat watch`, so the daemon keeps the STALE provenance state\n // current: once OBSERVED traffic quiets, edges past their threshold decay to\n // STALE instead of sitting live forever. Must be stopped alongside\n // stopPersist so no interval leaks when the slot is torn down or replaced.\n stopStaleness: () => void\n // Stops every connector poll loop registered for this slot (connectors/\n // index.ts's startConnectorPollLoop, one per opts.connectors entry). Same\n // lifecycle as stopStaleness — must run alongside it wherever the slot is\n // torn down or replaced.\n stopConnectors: () => 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.stopStaleness()\n } catch {\n // best-effort\n }\n try {\n slot.stopConnectors()\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(\n entry: RegistryEntry,\n connectors: ConnectorRegistration[] = [],\n): 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 stopStaleness: () => {},\n stopConnectors: () => {},\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 // Keep the STALE provenance state maintained on the shipped daemon path,\n // the same way `neat watch` does. Once OBSERVED traffic quiets, this loop\n // ticks markStaleEdges so edges past their threshold decay to STALE and the\n // transition lands in this slot's stale-events.ndjson — exactly where the\n // REST `/stale-events` route reads them back from.\n const stopStaleness = startStalenessLoop(graph, {\n staleEventsPath: paths.staleEventsPath,\n project: entry.name,\n })\n // Connectors plane (docs/contracts/connectors.md, ADR-124) — one poll\n // loop per registered connector, same interval-loop shape as the\n // staleness loop above and torn down alongside it (teardownSlot).\n const stopFns = connectors.map((registration) =>\n startConnectorPollLoop(\n registration.connector,\n { projectDir: entry.path, credentials: registration.credentials },\n graph,\n registration.resolveTarget,\n { intervalMs: registration.intervalMs },\n ),\n )\n const stopConnectors = (): void => {\n for (const stop of stopFns) stop()\n }\n await touchLastSeen(entry.name).catch(() => {})\n\n return {\n entry,\n graph,\n outPath,\n paths,\n stopPersist,\n stopStaleness,\n stopConnectors,\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, opts.connectors ?? [])\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, opts.connectors ?? [])\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","// Connectors plane — the provider-agnostic pull/map/fuse pipeline\n// (docs/contracts/connectors.md, docs/connectors/README.md, ADR-124).\n//\n// A provider module (packages/core/src/connectors/<provider>/, none shipped\n// yet) owns exactly two things: fetching `ObservedSignal[]` off its own API\n// (`ObservedConnector.poll`) and mapping a signal's targetKind/targetName to\n// a NEAT node id (the `resolveTarget` callback below). Everything after that\n// — resolving a static call site, minting the OBSERVED edge — is written\n// once, here, and is identical for every provider: a connector-sourced edge\n// and a span-sourced edge carry the same provenance and land through the\n// same mutation primitives OTel ingest uses (README.md's opening paragraph).\n//\n// Mutation authority (ADR-030 — see contracts.test.ts's \"Lifecycle contract\"\n// audit): only ingest.ts and extract/* may call a graph mutator directly.\n// This module never does — every node/edge write below goes through an\n// ingest.ts primitive (`ensureServiceNode`, `ensureObservedFileNode`,\n// `upsertObservedEdge`), exported for exactly this reuse. A future\n// provider's own target-node creation (a Supabase table InfraNode, say)\n// belongs in ingest.ts too, for the same reason — this module only ever\n// calls into it, never mutates the graph itself.\n\nimport type { EdgeTypeValue } from '@neat.is/types'\nimport type { NeatGraph } from '../graph.js'\nimport {\n ensureObservedFileNode,\n ensureServiceNode,\n reconcileObservedRelPath,\n upsertObservedEdge,\n type CallSite,\n} from '../ingest.js'\nimport type { ConnectorContext, ObservedConnector, ObservedSignal } from './types.js'\n\nexport type {\n ConnectorCallSite,\n ConnectorContext,\n ObservedConnector,\n ObservedSignal,\n} from './types.js'\n\n// env-unscoped, matching the sentinel identity.ts uses for \"no deployment\n// signal\" (ADR-074 §2) — a connector signal carries no\n// deployment.environment(.name) the way an OTel span might.\nconst NO_ENV = 'unknown'\n\n/**\n * What a provider's target-resolution step hands back for one signal. The\n * generic pipeline needs both endpoints of the edge it's about to mint:\n *\n * - `serviceName` is the NEAT manifest service whose code produced the\n * signal — the edge's source. The shared pipeline turns it into a plain\n * ServiceNode id, or a FileNode id once the fuse step below resolves the\n * signal's callSite against it.\n * - `targetNodeId` is the id the provider's own mapping already resolved —\n * an `infraId(...)` sub-resource, a RouteNode, a ServiceNode, whatever\n * (see each provider's docs/connectors/<provider>.md §Fusion).\n *\n * Returning `null` skips the signal honestly: an unresolvable target never\n * fabricates a node or edge (the same discipline file-awareness.md §6\n * states for OTel ingest).\n */\nexport interface ResolvedConnectorTarget {\n targetNodeId: string\n serviceName: string\n edgeType: EdgeTypeValue\n}\n\nexport type ResolveConnectorTarget = (\n signal: ObservedSignal,\n ctx: ConnectorContext,\n) => ResolvedConnectorTarget | null\n\nexport interface ConnectorPollResult {\n // Signals connector.poll() returned this tick.\n signalCount: number\n // Fresh OBSERVED edges minted.\n edgesCreated: number\n // Existing OBSERVED edges whose signal block advanced.\n edgesUpdated: number\n // Signals that resolved to no target (resolveTarget returned null, or the\n // resolved target/service node doesn't exist in the graph yet) — dropped\n // honestly rather than minting a fabricated edge.\n unresolved: number\n}\n\n/**\n * One poll cycle: fetch, map, fuse, mint. Pure with respect to `ctx` — the\n * caller (`startConnectorPollLoop` below, or a one-shot `neat sync`) owns\n * advancing `ctx.since` between calls.\n */\nexport async function runConnectorPoll(\n connector: ObservedConnector,\n ctx: ConnectorContext,\n graph: NeatGraph,\n resolveTarget: ResolveConnectorTarget,\n): Promise<ConnectorPollResult> {\n const signals = await connector.poll(ctx)\n let edgesCreated = 0\n let edgesUpdated = 0\n let unresolved = 0\n\n for (const signal of signals) {\n const resolved = resolveTarget(signal, ctx)\n if (!resolved) {\n unresolved++\n continue\n }\n\n // Same shape ingest.ts's handleSpan uses for every span: auto-create a\n // minimal ServiceNode the first time this service is seen so the edge\n // upsert below always has a source endpoint, even for a service the\n // static extractor hasn't reached (or never will, in an OTel-less setup).\n const serviceNodeId = ensureServiceNode(graph, resolved.serviceName, NO_ENV)\n\n // File-grain fusion when the signal carries a call site, through the\n // same reconcileObservedRelPath path OTel ingest uses (file-awareness.md\n // §4) — service-level, honestly, when it doesn't (§6, never fabricated).\n const callSite: CallSite | undefined = signal.callSite\n ? { relPath: signal.callSite.file, line: signal.callSite.line }\n : undefined\n const sourceId = callSite\n ? ensureObservedFileNode(graph, resolved.serviceName, serviceNodeId, callSite)\n : serviceNodeId\n const evidence = callSite\n ? {\n file: reconcileObservedRelPath(graph, resolved.serviceName, callSite.relPath),\n line: callSite.line,\n }\n : undefined\n\n // upsertObservedEdge increments its signal block by exactly one call per\n // invocation — the right unit for a single span, but a connector signal\n // is already an aggregate over the whole poll window (`callCount` calls,\n // `errorCount` of them failing). Replay it that many times so the\n // edge's spanCount/errorCount — and the confidence grade derived from\n // them, ADR-066 — land the same as if `callCount` individual spans had\n // arrived, rather than undercounting a batched signal down to +1.\n const calls = Math.trunc(signal.callCount)\n if (calls < 1) continue // nothing observed this window — not even a miss\n const errors = Math.min(Math.max(Math.trunc(signal.errorCount), 0), calls)\n\n let created = false\n let ok = true\n for (let i = 0; i < calls; i++) {\n const result = upsertObservedEdge(\n graph,\n resolved.edgeType,\n sourceId,\n resolved.targetNodeId,\n signal.lastObservedIso,\n i < errors,\n evidence,\n )\n if (!result) {\n // Target node doesn't exist yet — an extractor gap (supabase.md\n // §Static extractor gap documents exactly this case) or a provider\n // that hasn't minted it. Honest miss, not a crash.\n ok = false\n break\n }\n if (i === 0) created = result.created\n }\n if (!ok) {\n unresolved++\n continue\n }\n if (created) edgesCreated++\n else edgesUpdated++\n }\n\n return { signalCount: signals.length, edgesCreated, edgesUpdated, unresolved }\n}\n\nexport interface ConnectorPollLoopOptions {\n intervalMs?: number\n onError?: (err: unknown) => void\n}\n\nconst DEFAULT_POLL_INTERVAL_MS = 60_000\n\n/**\n * Recurring wrapper around `runConnectorPoll` — the same setInterval +\n * unref + try/catch + stop-closure shape `startStalenessLoop` (ingest.ts)\n * already uses for the daemon's per-project background tasks, reused here\n * rather than reinvented. `daemon.ts` wires this in exactly where it wires\n * `startStalenessLoop`, tearing both down together per project slot.\n *\n * Advances `since` to the tick's own start time after every successful\n * poll — the next tick asks the provider for \"since last tick\" rather than\n * replaying. A tick that throws logs and leaves `since` where it was, so a\n * transient provider outage doesn't silently skip the gap once it recovers.\n */\nexport function startConnectorPollLoop(\n connector: ObservedConnector,\n ctx: ConnectorContext,\n graph: NeatGraph,\n resolveTarget: ResolveConnectorTarget,\n options: ConnectorPollLoopOptions = {},\n): () => void {\n let stopped = false\n let since = ctx.since\n const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS\n const onError =\n options.onError ??\n ((err: unknown) => console.error(`[neatd] connector poll failed (${connector.provider})`, err))\n\n const tick = (): void => {\n if (stopped) return\n void (async () => {\n const tickStartedAt = new Date().toISOString()\n try {\n await runConnectorPoll(connector, { ...ctx, since }, graph, resolveTarget)\n since = tickStartedAt\n } catch (err) {\n onError(err)\n }\n })()\n }\n\n const interval = setInterval(tick, intervalMs)\n if (typeof interval.unref === 'function') interval.unref()\n return () => {\n stopped = true\n clearInterval(interval)\n }\n}\n\n/**\n * One project's registered connector, ready for `daemon.ts` to poll on an\n * interval. Deliberately thin — no config-loading or credential-broker logic\n * lives here (that's provider- and profile-specific, later work per\n * docs/contracts/connectors.md §3); this is just the seam a daemon slot\n * wires a connector through.\n */\nexport interface ConnectorRegistration {\n connector: ObservedConnector\n credentials: Record<string, unknown>\n resolveTarget: ResolveConnectorTarget\n intervalMs?: number\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"]}
|