@neat.is/core 0.7.7 → 0.7.9
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-BIY46Q6U.js → chunk-BC53SCT7.js} +36 -2
- package/dist/chunk-BC53SCT7.js.map +1 -0
- package/dist/{chunk-4GCV46AP.js → chunk-FCO5Z3RW.js} +2 -2
- package/dist/{chunk-YVZRBT4C.js → chunk-ILG3SMD5.js} +54 -4
- package/dist/{chunk-YVZRBT4C.js.map → chunk-ILG3SMD5.js.map} +1 -1
- package/dist/{chunk-UI3AFJAF.js → chunk-N5TPODCX.js} +171 -49
- package/dist/chunk-N5TPODCX.js.map +1 -0
- package/dist/{chunk-Y43UCVZS.js → chunk-UUYCTH2E.js} +24 -8
- package/dist/chunk-UUYCTH2E.js.map +1 -0
- package/dist/cli.cjs +238 -63
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +17 -14
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +242 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +242 -54
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-DPSAGRUU.js → otel-grpc-CO47JRIN.js} +3 -3
- package/dist/server.cjs +226 -54
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +4 -4
- package/package.json +2 -2
- package/dist/chunk-BIY46Q6U.js.map +0 -1
- package/dist/chunk-UI3AFJAF.js.map +0 -1
- package/dist/chunk-Y43UCVZS.js.map +0 -1
- /package/dist/{chunk-4GCV46AP.js.map → chunk-FCO5Z3RW.js.map} +0 -0
- /package/dist/{otel-grpc-DPSAGRUU.js.map → otel-grpc-CO47JRIN.js.map} +0 -0
|
@@ -280,12 +280,46 @@ var SubstringIndex = class {
|
|
|
280
280
|
this.graph = graph;
|
|
281
281
|
}
|
|
282
282
|
};
|
|
283
|
+
var DEFAULT_SEARCH_INIT_TIMEOUT_MS = 3e4;
|
|
284
|
+
function searchInitTimeoutMs() {
|
|
285
|
+
const env = process.env.NEAT_SEARCH_INIT_TIMEOUT_MS;
|
|
286
|
+
if (env !== void 0 && env.length > 0) {
|
|
287
|
+
const n = Number.parseInt(env, 10);
|
|
288
|
+
if (Number.isFinite(n) && n >= 0) return n;
|
|
289
|
+
}
|
|
290
|
+
return DEFAULT_SEARCH_INIT_TIMEOUT_MS;
|
|
291
|
+
}
|
|
292
|
+
async function resolveEmbedderBounded(factory, timeoutMs) {
|
|
293
|
+
if (timeoutMs <= 0) return factory();
|
|
294
|
+
let timer;
|
|
295
|
+
const TIMED_OUT = /* @__PURE__ */ Symbol("embedder-init-timeout");
|
|
296
|
+
const timeout = new Promise((resolve) => {
|
|
297
|
+
timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
|
|
298
|
+
timer.unref?.();
|
|
299
|
+
});
|
|
300
|
+
try {
|
|
301
|
+
const result = await Promise.race([factory(), timeout]);
|
|
302
|
+
if (result === TIMED_OUT) {
|
|
303
|
+
console.warn(
|
|
304
|
+
`semantic_search: embedder init exceeded ${timeoutMs}ms; falling back to substring search. Set NEAT_SEARCH_INIT_TIMEOUT_MS to raise the bound (or 0 to wait indefinitely).`
|
|
305
|
+
);
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
return result;
|
|
309
|
+
} finally {
|
|
310
|
+
if (timer) clearTimeout(timer);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
283
313
|
async function buildSearchIndex(graph, options = {}) {
|
|
284
314
|
let embedder = null;
|
|
285
315
|
if (options.embedder) {
|
|
286
316
|
embedder = options.embedder;
|
|
287
317
|
} else if (options.forceProvider !== "substring") {
|
|
288
|
-
|
|
318
|
+
const factory = options.embedderFactory ?? pickEmbedder;
|
|
319
|
+
embedder = await resolveEmbedderBounded(
|
|
320
|
+
factory,
|
|
321
|
+
options.initTimeoutMs ?? searchInitTimeoutMs()
|
|
322
|
+
);
|
|
289
323
|
if (options.forceProvider === "ollama" && embedder?.provider !== "ollama") {
|
|
290
324
|
embedder = null;
|
|
291
325
|
}
|
|
@@ -311,4 +345,4 @@ async function buildSearchIndex(graph, options = {}) {
|
|
|
311
345
|
export {
|
|
312
346
|
buildSearchIndex
|
|
313
347
|
};
|
|
314
|
-
//# sourceMappingURL=chunk-
|
|
348
|
+
//# sourceMappingURL=chunk-BC53SCT7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/search.ts"],"sourcesContent":["// semantic_search — embedding-based node retrieval with a three-tier\n// fallback chain. The chain is settled in ADR-025; this file is the\n// implementation. Public API:\n//\n// buildSearchIndex(graph, opts) → SearchIndex\n// SearchIndex.search(query, limit) → { provider, matches }\n// SearchIndex.refresh(graph) → re-embeds new/changed nodes,\n// drops vanished ones\n//\n// The `/search` route in api.ts holds a single SearchIndex, refreshing it\n// after any extraction. MCP's `semantic_search` tool reads the same shape.\n\nimport { promises as fs } from 'node:fs'\nimport path from 'node:path'\nimport { createHash } from 'node:crypto'\nimport type { GraphNode } from '@neat.is/types'\nimport type { NeatGraph } from './graph.js'\n\nexport interface ScoredNode {\n node: GraphNode\n score: number\n}\n\nexport interface SearchResponse {\n query: string\n provider: 'ollama' | 'transformers' | 'substring'\n matches: ScoredNode[]\n}\n\nexport interface SearchIndex {\n readonly provider: SearchResponse['provider']\n search(query: string, limit?: number): Promise<SearchResponse>\n refresh(graph: NeatGraph): Promise<void>\n}\n\ninterface Embedder {\n provider: 'ollama' | 'transformers'\n model: string\n dim: number\n embed(texts: string[]): Promise<Float32Array[]>\n}\n\nconst DEFAULT_LIMIT = 10\nconst NOMIC_DIM = 768\nconst MINI_LM_DIM = 384\n\n// FrontierNodes are noise by design (placeholders that should disappear).\n// Embedding them would just clutter results.\nfunction shouldEmbed(node: GraphNode): boolean {\n return node.type !== 'FrontierNode'\n}\n\n// Deterministic per-node text. Stable keys let the cache hit across\n// extractions when nothing material changed.\nexport function embedText(node: GraphNode): string {\n const parts: string[] = [node.id]\n const name = (node as { name?: string }).name\n if (name) parts.push(name)\n switch (node.type) {\n case 'ServiceNode': {\n const lang = (node as { language?: string }).language\n if (lang) parts.push(`language=${lang}`)\n break\n }\n case 'DatabaseNode': {\n const eng = (node as { engine?: string }).engine\n const ver = (node as { engineVersion?: string }).engineVersion\n if (eng) parts.push(`engine=${eng}`)\n if (ver) parts.push(`engineVersion=${ver}`)\n break\n }\n case 'InfraNode': {\n const kind = (node as { kind?: string }).kind\n if (kind) parts.push(`kind=${kind}`)\n break\n }\n case 'ConfigNode': {\n const filePath = (node as { path?: string }).path\n if (filePath) parts.push(`path=${filePath}`)\n break\n }\n case 'RouteNode': {\n const method = (node as { method?: string }).method\n const tmpl = (node as { pathTemplate?: string }).pathTemplate\n if (method) parts.push(`method=${method}`)\n if (tmpl) parts.push(`path=${tmpl}`)\n break\n }\n case 'GraphQLOperationNode': {\n const opType = (node as { operationType?: string }).operationType\n if (opType) parts.push(`operationType=${opType}`)\n break\n }\n case 'GrpcMethodNode': {\n const rpcService = (node as { rpcService?: string }).rpcService\n const rpcMethod = (node as { rpcMethod?: string }).rpcMethod\n if (rpcService) parts.push(`rpcService=${rpcService}`)\n if (rpcMethod) parts.push(`rpcMethod=${rpcMethod}`)\n break\n }\n case 'WebSocketChannelNode': {\n const channel = (node as { channel?: string }).channel\n if (channel) parts.push(`channel=${channel}`)\n break\n }\n default:\n break\n }\n return parts.join(' ')\n}\n\nfunction attrsHash(node: GraphNode): string {\n return createHash('sha1').update(embedText(node)).digest('hex').slice(0, 16)\n}\n\nexport function cosine(a: Float32Array, b: Float32Array): number {\n if (a.length !== b.length) return 0\n let dot = 0\n let na = 0\n let nb = 0\n for (let i = 0; i < a.length; i++) {\n const ai = a[i] ?? 0\n const bi = b[i] ?? 0\n dot += ai * bi\n na += ai * ai\n nb += bi * bi\n }\n if (na === 0 || nb === 0) return 0\n return dot / (Math.sqrt(na) * Math.sqrt(nb))\n}\n\n// ---------------------------------------------------------------- Embedders\n\nfunction ollamaHost(): string | null {\n return process.env.OLLAMA_HOST ?? null\n}\n\nasync function ollamaReachable(host: string): Promise<boolean> {\n try {\n const res = await fetch(`${host.replace(/\\/$/, '')}/api/tags`, {\n signal: AbortSignal.timeout(500),\n })\n return res.ok\n } catch {\n return false\n }\n}\n\nfunction makeOllamaEmbedder(host: string, model = 'nomic-embed-text'): Embedder {\n const root = host.replace(/\\/$/, '')\n return {\n provider: 'ollama',\n model,\n dim: NOMIC_DIM,\n async embed(texts: string[]): Promise<Float32Array[]> {\n const out: Float32Array[] = []\n // Ollama's /api/embeddings is one-text-per-request. ≤10K nodes × ~30ms\n // each is fine for a one-shot index build; if it ever isn't, the API\n // also accepts batched input on /api/embed (newer routes).\n for (const text of texts) {\n const res = await fetch(`${root}/api/embeddings`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ model, prompt: text }),\n })\n if (!res.ok) {\n throw new Error(`ollama embeddings: ${res.status} ${res.statusText}`)\n }\n const data = (await res.json()) as { embedding: number[] }\n out.push(Float32Array.from(data.embedding))\n }\n return out\n },\n }\n}\n\ninterface XenovaPipeline {\n (text: string | string[], options?: { pooling?: string; normalize?: boolean }): Promise<{\n data: Float32Array\n }>\n}\n\nasync function makeTransformersEmbedder(): Promise<Embedder | null> {\n let pipelineFn: ((task: string, model: string) => Promise<XenovaPipeline>) | null = null\n try {\n // Lazy require so server.ts boot doesn't pay the WASM init cost when\n // Ollama is available. The package is heavy — only load it on demand.\n // The package is optional so its types may not be installed in every\n // environment. Use a dynamic specifier so tsc keeps the import dynamic\n // and doesn't try to resolve types at build time.\n const specifier = '@xenova/transformers'\n const mod = (await import(specifier)) as unknown as {\n pipeline: (task: string, model: string) => Promise<XenovaPipeline>\n }\n pipelineFn = mod.pipeline\n } catch {\n return null\n }\n if (!pipelineFn) return null\n const model = 'Xenova/all-MiniLM-L6-v2'\n const extractor = await pipelineFn('feature-extraction', model)\n return {\n provider: 'transformers',\n model,\n dim: MINI_LM_DIM,\n async embed(texts: string[]): Promise<Float32Array[]> {\n const out: Float32Array[] = []\n for (const text of texts) {\n // Mean-pooled, L2-normalized → cosine reduces to dot product but\n // we keep the explicit cosine() for clarity.\n const result = await extractor(text, { pooling: 'mean', normalize: true })\n out.push(Float32Array.from(result.data))\n }\n return out\n },\n }\n}\n\n// Picks the highest-tier embedder available. Returns null when only\n// substring is available (caller decides what to build).\nexport async function pickEmbedder(): Promise<Embedder | null> {\n const host = ollamaHost()\n if (host && (await ollamaReachable(host))) {\n return makeOllamaEmbedder(host)\n }\n return makeTransformersEmbedder()\n}\n\n// ------------------------------------------------------------------ Cache\n\ninterface CacheEntry {\n nodeId: string\n attrsHash: string\n vector: number[]\n}\n\ninterface CacheFile {\n version: 1\n provider: 'ollama' | 'transformers'\n model: string\n dim: number\n entries: CacheEntry[]\n}\n\nasync function readCache(cachePath: string): Promise<CacheFile | null> {\n try {\n const raw = await fs.readFile(cachePath, 'utf8')\n const parsed = JSON.parse(raw) as CacheFile\n if (parsed.version !== 1) return null\n return parsed\n } catch {\n return null\n }\n}\n\nasync function writeCache(cachePath: string, cache: CacheFile): Promise<void> {\n await fs.mkdir(path.dirname(cachePath), { recursive: true })\n await fs.writeFile(cachePath, JSON.stringify(cache))\n}\n\n// ----------------------------------------------------------------- Indexes\n\nclass VectorIndex implements SearchIndex {\n readonly provider: 'ollama' | 'transformers'\n private vectors = new Map<string, { node: GraphNode; vector: Float32Array; hash: string }>()\n\n constructor(\n private embedder: Embedder,\n private cachePath: string | null,\n ) {\n this.provider = embedder.provider\n }\n\n async search(query: string, limit = DEFAULT_LIMIT): Promise<SearchResponse> {\n const trimmed = query.trim()\n if (!trimmed || this.vectors.size === 0) {\n return { query: trimmed, provider: this.provider, matches: [] }\n }\n const embedded = await this.embedder.embed([trimmed])\n const qv = embedded[0]\n if (!qv) {\n return { query: trimmed, provider: this.provider, matches: [] }\n }\n const scored: ScoredNode[] = []\n for (const { node, vector } of this.vectors.values()) {\n const score = cosine(qv, vector)\n scored.push({ node, score })\n }\n scored.sort((a, b) => b.score - a.score)\n return { query: trimmed, provider: this.provider, matches: scored.slice(0, limit) }\n }\n\n async refresh(graph: NeatGraph): Promise<void> {\n const present = new Set<string>()\n const toEmbed: { id: string; node: GraphNode; hash: string; text: string }[] = []\n\n graph.forEachNode((id, attrs) => {\n const node = attrs as GraphNode\n if (!shouldEmbed(node)) return\n present.add(id)\n const hash = attrsHash(node)\n const cached = this.vectors.get(id)\n if (cached && cached.hash === hash) {\n cached.node = node\n return\n }\n toEmbed.push({ id, node, hash, text: embedText(node) })\n })\n\n // Drop vanished nodes\n for (const id of [...this.vectors.keys()]) {\n if (!present.has(id)) this.vectors.delete(id)\n }\n\n if (toEmbed.length > 0) {\n const vectors = await this.embedder.embed(toEmbed.map((e) => e.text))\n toEmbed.forEach((entry, i) => {\n const v = vectors[i]\n if (!v) return\n this.vectors.set(entry.id, { node: entry.node, vector: v, hash: entry.hash })\n })\n }\n\n if (this.cachePath) {\n const entries: CacheEntry[] = []\n for (const [id, { vector, hash }] of this.vectors) {\n entries.push({ nodeId: id, attrsHash: hash, vector: Array.from(vector) })\n }\n await writeCache(this.cachePath, {\n version: 1,\n provider: this.embedder.provider,\n model: this.embedder.model,\n dim: this.embedder.dim,\n entries,\n })\n }\n }\n\n // Hydrate the in-memory map from a previously-written cache. Validates\n // shape against the current embedder; mismatch → empty start.\n loadFromCache(cache: CacheFile, graph: NeatGraph): void {\n if (\n cache.provider !== this.embedder.provider ||\n cache.model !== this.embedder.model ||\n cache.dim !== this.embedder.dim\n ) {\n return\n }\n const present = new Map<string, GraphNode>()\n graph.forEachNode((id, attrs) => {\n const node = attrs as GraphNode\n if (shouldEmbed(node)) present.set(id, node)\n })\n for (const entry of cache.entries) {\n const node = present.get(entry.nodeId)\n if (!node) continue\n // Skip cache entries whose attrs no longer match — they'll be\n // re-embedded by the next refresh().\n if (attrsHash(node) !== entry.attrsHash) continue\n if (entry.vector.length !== this.embedder.dim) continue\n this.vectors.set(entry.nodeId, {\n node,\n hash: entry.attrsHash,\n vector: Float32Array.from(entry.vector),\n })\n }\n }\n}\n\nclass SubstringIndex implements SearchIndex {\n readonly provider = 'substring' as const\n private graph: NeatGraph | null = null\n\n async search(query: string, limit = DEFAULT_LIMIT): Promise<SearchResponse> {\n const q = query.trim().toLowerCase()\n const out: ScoredNode[] = []\n if (!q || !this.graph) {\n return { query: q, provider: 'substring', matches: [] }\n }\n this.graph.forEachNode((id, attrs) => {\n const node = attrs as GraphNode\n const name = (node as { name?: string }).name ?? ''\n if (id.toLowerCase().includes(q) || name.toLowerCase().includes(q)) {\n out.push({ node, score: 1 })\n }\n })\n return { query: q, provider: 'substring', matches: out.slice(0, limit) }\n }\n\n async refresh(graph: NeatGraph): Promise<void> {\n this.graph = graph\n }\n}\n\n// --------------------------------------------------------- Bounded init (#819)\n\n// How long to wait for the embedder to initialize before giving up and falling\n// back to the substring index. `pickEmbedder` reaches makeTransformersEmbedder,\n// which loads an ONNX/WASM runtime and — on a cold cache — downloads the model.\n// Either can stall: an offline or throttled network on first run, or a\n// pathological native-init on a runtime NEAT doesn't target (the Node-26 report\n// in #819). Left unbounded, that stall hangs the whole `neat watch` / dev-server\n// bring-up with no output — the worst UX, since the operator sees neither the\n// receiver come up nor an error. Bounding it turns the stall into one warning\n// line plus a substring-search fallback, so the graph stays queryable and the\n// daemon still comes up. Generous by default so a legitimately slow first-run\n// model download still lands; NEAT_SEARCH_INIT_TIMEOUT_MS overrides, and `0`\n// restores the old wait-forever behaviour for anyone who wants it.\nconst DEFAULT_SEARCH_INIT_TIMEOUT_MS = 30_000\n\nfunction searchInitTimeoutMs(): number {\n const env = process.env.NEAT_SEARCH_INIT_TIMEOUT_MS\n if (env !== undefined && env.length > 0) {\n const n = Number.parseInt(env, 10)\n if (Number.isFinite(n) && n >= 0) return n\n }\n return DEFAULT_SEARCH_INIT_TIMEOUT_MS\n}\n\n// Resolve an embedder, bounded by `timeoutMs`. A non-positive timeout means no\n// bound (wait for the factory however long it takes). On timeout we log a single\n// warning naming the stall and return null — the caller then builds the\n// substring index instead of hanging. The factory promise is left to settle in\n// the background (harmless: nothing downstream awaits it, and a later refresh\n// rebuilds the vector index if the process is still up); the point is that the\n// bring-up never blocks on it past the bound. A factory that rejects propagates\n// unchanged — that's an already-handled failure mode (buildSearchIndex's callers\n// fall back to substring on a throw); only the never-settles case is new here.\nasync function resolveEmbedderBounded(\n factory: () => Promise<Embedder | null>,\n timeoutMs: number,\n): Promise<Embedder | null> {\n if (timeoutMs <= 0) return factory()\n let timer: ReturnType<typeof setTimeout> | undefined\n const TIMED_OUT = Symbol('embedder-init-timeout')\n const timeout = new Promise<typeof TIMED_OUT>((resolve) => {\n timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs)\n // Don't let the fallback timer keep the event loop alive on its own.\n timer.unref?.()\n })\n try {\n const result = await Promise.race([factory(), timeout])\n if (result === TIMED_OUT) {\n console.warn(\n `semantic_search: embedder init exceeded ${timeoutMs}ms; falling back to substring search. ` +\n `Set NEAT_SEARCH_INIT_TIMEOUT_MS to raise the bound (or 0 to wait indefinitely).`,\n )\n return null\n }\n return result\n } finally {\n if (timer) clearTimeout(timer)\n }\n}\n\n// ------------------------------------------------------------ Public factory\n\nexport interface BuildSearchIndexOptions {\n // Where to read/write the embedding cache. Falls back to in-memory only\n // if not provided. Pass `null` to explicitly disable caching.\n cachePath?: string | null\n // Override the embedder selection. Useful for tests (substring-only mode\n // skips the Ollama probe + the Transformers.js download).\n forceProvider?: 'ollama' | 'transformers' | 'substring'\n // Pre-built embedder (test injection). Wins over forceProvider.\n embedder?: Embedder\n // Override the embedder resolver (defaults to `pickEmbedder`, the\n // Ollama → transformers chain). Test seam for the init-timeout fallback\n // below — inject a factory that stalls to prove the bring-up degrades to\n // substring instead of hanging (#819), without a real model load.\n embedderFactory?: () => Promise<Embedder | null>\n // Upper bound on embedder init before falling back to substring (#819).\n // Defaults to NEAT_SEARCH_INIT_TIMEOUT_MS or 30s; `0` waits indefinitely.\n initTimeoutMs?: number\n}\n\nexport async function buildSearchIndex(\n graph: NeatGraph,\n options: BuildSearchIndexOptions = {},\n): Promise<SearchIndex> {\n let embedder: Embedder | null = null\n if (options.embedder) {\n embedder = options.embedder\n } else if (options.forceProvider !== 'substring') {\n // Bound the embedder init so a stalled model load / native init can't hang\n // the bring-up — it degrades to substring instead (#819).\n const factory = options.embedderFactory ?? pickEmbedder\n embedder = await resolveEmbedderBounded(\n factory,\n options.initTimeoutMs ?? searchInitTimeoutMs(),\n )\n if (options.forceProvider === 'ollama' && embedder?.provider !== 'ollama') {\n embedder = null\n }\n if (options.forceProvider === 'transformers' && embedder?.provider !== 'transformers') {\n embedder = null\n }\n }\n\n if (!embedder) {\n const idx = new SubstringIndex()\n await idx.refresh(graph)\n return idx\n }\n\n const cachePath = options.cachePath === undefined ? null : options.cachePath\n const idx = new VectorIndex(embedder, cachePath)\n if (cachePath) {\n const cache = await readCache(cachePath)\n if (cache) idx.loadFromCache(cache, graph)\n }\n await idx.refresh(graph)\n return idx\n}\n"],"mappings":"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","names":["idx"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
parseOtlpRequest
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-UUYCTH2E.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-FCO5Z3RW.js.map
|
|
@@ -20,13 +20,13 @@ import {
|
|
|
20
20
|
startStalenessLoop,
|
|
21
21
|
touchLastSeen,
|
|
22
22
|
writeAtomically
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-N5TPODCX.js";
|
|
24
24
|
import {
|
|
25
25
|
assertBindAuthority,
|
|
26
26
|
buildOtelReceiver,
|
|
27
27
|
listenSteppingOtlp,
|
|
28
28
|
readAuthEnv
|
|
29
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-UUYCTH2E.js";
|
|
30
30
|
|
|
31
31
|
// src/daemon.ts
|
|
32
32
|
import {
|
|
@@ -501,6 +501,19 @@ async function startDaemon(opts = {}) {
|
|
|
501
501
|
let otlpAddress = "";
|
|
502
502
|
let daemonRecord = null;
|
|
503
503
|
if (bind) {
|
|
504
|
+
let bareSpanIsRoutable2 = function(serviceName) {
|
|
505
|
+
if (singleProject) {
|
|
506
|
+
const slot = slots.get(singleProject);
|
|
507
|
+
if (!slot) {
|
|
508
|
+
return !serviceName || serviceNameMatchesProject(serviceName, singleProject);
|
|
509
|
+
}
|
|
510
|
+
return spanBelongsToSingleProject(slot.graph, singleProject, serviceName);
|
|
511
|
+
}
|
|
512
|
+
const entries = [...slots.values()].map((s) => s.entry);
|
|
513
|
+
const target = routeSpanToProject(serviceName, entries);
|
|
514
|
+
return slots.has(target) || slots.has(DEFAULT_PROJECT);
|
|
515
|
+
};
|
|
516
|
+
var bareSpanIsRoutable = bareSpanIsRoutable2;
|
|
504
517
|
const auth = readAuthEnv();
|
|
505
518
|
const host = resolveHost(opts, Boolean(auth.authToken));
|
|
506
519
|
const restPort = resolveRestPort(opts);
|
|
@@ -598,6 +611,20 @@ async function startDaemon(opts = {}) {
|
|
|
598
611
|
const liveEntries = await listProjects().catch(() => []);
|
|
599
612
|
let slot = slots.get(project);
|
|
600
613
|
if (!slot) {
|
|
614
|
+
if (singleProject && project === singleProject) {
|
|
615
|
+
slot = await tryRecoverSlot({
|
|
616
|
+
name: singleProject,
|
|
617
|
+
path: singleProjectPath,
|
|
618
|
+
registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
619
|
+
languages: [],
|
|
620
|
+
status: "active"
|
|
621
|
+
});
|
|
622
|
+
if (!slot || slot.status !== "active") {
|
|
623
|
+
warnDroppedSpan(singleProject, slot?.errorReason ?? "unknown");
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
return slot;
|
|
627
|
+
}
|
|
601
628
|
await recordUnroutedSpan(serviceName, traceId);
|
|
602
629
|
return null;
|
|
603
630
|
}
|
|
@@ -664,7 +691,30 @@ async function startDaemon(opts = {}) {
|
|
|
664
691
|
// host, rather than accepting it and dropping the batch. `slots` covers
|
|
665
692
|
// active/recovering projects, `bootstrapStatus` the ones still
|
|
666
693
|
// extracting; a foreign or wrong-cased project name matches neither.
|
|
667
|
-
|
|
694
|
+
// A single-project daemon owns exactly one project by definition, so its
|
|
695
|
+
// own name always counts as registered even before loadAll populates the
|
|
696
|
+
// slot — otherwise a scoped span arriving during cold-start would 404 and
|
|
697
|
+
// be lost (OTLP does not retry 4xx). resolveSlotByName then builds it (#879).
|
|
698
|
+
isProjectRegistered: (project) => slots.has(project) || bootstrapStatus.has(project) || singleProject !== void 0 && project === singleProject,
|
|
699
|
+
// #881 — the bare `/v1/traces` route replies before the span is routed
|
|
700
|
+
// (off the queue), so tell the receiver, per batch, how many spans will
|
|
701
|
+
// land on no project. It keeps the 200 but reports those as
|
|
702
|
+
// partialSuccess.rejectedSpans instead of an empty partialSuccess that an
|
|
703
|
+
// exporter reads as full acceptance. Pure — the drop + unrouted-ledger
|
|
704
|
+
// write still happen on the async onSpan path.
|
|
705
|
+
classifyBareRoutability: (spans) => {
|
|
706
|
+
let rejected = 0;
|
|
707
|
+
for (const span of spans) {
|
|
708
|
+
if (!bareSpanIsRoutable2(span.service)) rejected++;
|
|
709
|
+
}
|
|
710
|
+
if (rejected === 0) return { rejected: 0 };
|
|
711
|
+
const noun = rejected === 1 ? "span" : "spans";
|
|
712
|
+
const verb = rejected === 1 ? "was" : "were";
|
|
713
|
+
return {
|
|
714
|
+
rejected,
|
|
715
|
+
message: `${rejected} ${noun} matched no project on this daemon and ${verb} dropped. Export to /projects/<project>/v1/traces, or check the exporter's service.name.`
|
|
716
|
+
};
|
|
717
|
+
}
|
|
668
718
|
});
|
|
669
719
|
otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
|
|
670
720
|
console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);
|
|
@@ -837,4 +887,4 @@ export {
|
|
|
837
887
|
resolveHost,
|
|
838
888
|
startDaemon
|
|
839
889
|
};
|
|
840
|
-
//# sourceMappingURL=chunk-
|
|
890
|
+
//# sourceMappingURL=chunk-ILG3SMD5.js.map
|