@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/index.d.cts CHANGED
@@ -302,6 +302,10 @@ interface BuildOtelReceiverOptions {
302
302
  onProjectSpan?: ProjectSpanHandler;
303
303
  onProjectErrorSpanSync?: (project: string, span: ParsedSpan) => Promise<void>;
304
304
  isProjectRegistered?: (project: string) => boolean;
305
+ classifyBareRoutability?: (spans: ParsedSpan[]) => {
306
+ rejected: number;
307
+ message?: string;
308
+ };
305
309
  bodyLimit?: number;
306
310
  authToken?: string;
307
311
  trustProxy?: boolean;
package/dist/index.d.ts CHANGED
@@ -302,6 +302,10 @@ interface BuildOtelReceiverOptions {
302
302
  onProjectSpan?: ProjectSpanHandler;
303
303
  onProjectErrorSpanSync?: (project: string, span: ParsedSpan) => Promise<void>;
304
304
  isProjectRegistered?: (project: string) => boolean;
305
+ classifyBareRoutability?: (spans: ParsedSpan[]) => {
306
+ rejected: number;
307
+ message?: string;
308
+ };
305
309
  bodyLimit?: number;
306
310
  authToken?: string;
307
311
  trustProxy?: boolean;
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  routeSpanToProject,
3
3
  startDaemon
4
- } from "./chunk-YVZRBT4C.js";
4
+ } from "./chunk-ILG3SMD5.js";
5
5
  import {
6
6
  ProjectNameCollisionError,
7
7
  addProject,
@@ -37,15 +37,15 @@ import {
37
37
  thresholdForEdgeType,
38
38
  touchLastSeen,
39
39
  writeAtomically
40
- } from "./chunk-UI3AFJAF.js";
40
+ } from "./chunk-N5TPODCX.js";
41
41
  import {
42
42
  startOtelGrpcReceiver
43
- } from "./chunk-4GCV46AP.js";
43
+ } from "./chunk-FCO5Z3RW.js";
44
44
  import {
45
45
  buildOtelReceiver,
46
46
  logSpanHandler,
47
47
  parseOtlpRequest
48
- } from "./chunk-Y43UCVZS.js";
48
+ } from "./chunk-UUYCTH2E.js";
49
49
  export {
50
50
  ProjectNameCollisionError,
51
51
  addProject,
package/dist/neatd.cjs CHANGED
@@ -504,13 +504,18 @@ function loadProtobufResponseEncoder() {
504
504
  );
505
505
  return exportTraceServiceResponseType;
506
506
  }
507
- function encodeProtobufResponseBody() {
508
- if (cachedProtobufResponseBody) return cachedProtobufResponseBody;
507
+ function encodeProtobufResponseBody(rejected, message) {
509
508
  const Type = loadProtobufResponseEncoder();
510
- const msg = Type.create({});
511
- const encoded = Type.encode(msg).finish();
512
- cachedProtobufResponseBody = Buffer.from(encoded);
513
- return cachedProtobufResponseBody;
509
+ if (!rejected) {
510
+ if (cachedProtobufResponseBody) return cachedProtobufResponseBody;
511
+ const msg2 = Type.create({});
512
+ cachedProtobufResponseBody = Buffer.from(Type.encode(msg2).finish());
513
+ return cachedProtobufResponseBody;
514
+ }
515
+ const msg = Type.fromObject({
516
+ partial_success: { rejected_spans: rejected, error_message: message ?? "" }
517
+ });
518
+ return Buffer.from(Type.encode(msg).finish());
514
519
  }
515
520
  async function decodeProtobufBody(buf) {
516
521
  const Type = loadProtobufDecoder();
@@ -625,6 +630,13 @@ async function buildOtelReceiver(opts) {
625
630
  }
626
631
  return reply.code(200).header("content-type", "application/json").send({ partialSuccess: {} });
627
632
  }
633
+ function sendOtlpPartial(reply, flavor, rejected, message) {
634
+ if (flavor === "protobuf") {
635
+ const buf = encodeProtobufResponseBody(rejected, message);
636
+ return reply.code(200).header("content-type", "application/x-protobuf").send(buf);
637
+ }
638
+ return reply.code(200).header("content-type", "application/json").send({ partialSuccess: { rejectedSpans: rejected, errorMessage: message } });
639
+ }
628
640
  app.addContentTypeParser(
629
641
  "application/x-protobuf",
630
642
  { parseAs: "buffer", bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024 },
@@ -652,6 +664,10 @@ async function buildOtelReceiver(opts) {
652
664
  }
653
665
  }
654
666
  enqueue(spans);
667
+ if (opts.classifyBareRoutability) {
668
+ const { rejected, message } = opts.classifyBareRoutability(spans);
669
+ if (rejected > 0) return sendOtlpPartial(reply, result.flavor, rejected, message);
670
+ }
655
671
  return sendOtlpSuccess(reply, result.flavor);
656
672
  });
657
673
  app.post("/projects/:project/v1/traces", async (req2, reply) => {
@@ -3008,6 +3024,7 @@ var ROUTER_METHODS = /* @__PURE__ */ new Set([
3008
3024
  "all"
3009
3025
  ]);
3010
3026
  var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
3027
+ var NET_HTTP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
3011
3028
  var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
3012
3029
  function goRouterRoutesFromSource(source, parser, framework) {
3013
3030
  const tree = parseSource2(parser, source);
@@ -3061,6 +3078,101 @@ function echoRoutesFromSource(source, parser) {
3061
3078
  function fiberRoutesFromSource(source, parser) {
3062
3079
  return goRouterRoutesFromSource(source, parser, "fiber");
3063
3080
  }
3081
+ function chiRoutesFromSource(source, parser) {
3082
+ const tree = parseSource2(parser, source);
3083
+ const out = [];
3084
+ chiWalk(tree.rootNode, "", out);
3085
+ return out;
3086
+ }
3087
+ function stripChiRegex(path70) {
3088
+ return path70.replace(/\{([^{}:]+):[^{}]*\}/g, "{$1}");
3089
+ }
3090
+ function chiWalk(node, prefix, out) {
3091
+ for (let i = 0; i < node.namedChildCount; i++) {
3092
+ const child = node.namedChild(i);
3093
+ if (child) chiHandle(child, prefix, out);
3094
+ }
3095
+ }
3096
+ function chiHandle(node, prefix, out) {
3097
+ if (node.type === "call_expression") {
3098
+ const fn = node.childForFieldName("function");
3099
+ if (fn?.type === "selector_expression") {
3100
+ const field = fn.childForFieldName("field")?.text;
3101
+ const args = node.childForFieldName("arguments");
3102
+ if (field === "Route") {
3103
+ const leaf = goStringLiteral(args?.namedChild(0));
3104
+ const closure = args?.namedChild(1);
3105
+ if (leaf !== null && closure?.type === "func_literal") {
3106
+ const body = closure.childForFieldName("body");
3107
+ if (body) chiWalk(body, prefix + leaf, out);
3108
+ }
3109
+ return;
3110
+ }
3111
+ if (field === "Group") {
3112
+ const closure = args?.namedChild(0);
3113
+ if (closure?.type === "func_literal") {
3114
+ const body = closure.childForFieldName("body");
3115
+ if (body) chiWalk(body, prefix, out);
3116
+ }
3117
+ return;
3118
+ }
3119
+ if (field === "Mount") {
3120
+ return;
3121
+ }
3122
+ if (field && ROUTER_METHODS.has(field.toLowerCase())) {
3123
+ const leaf = goStringLiteral(args?.namedChild(0));
3124
+ if (leaf !== null) {
3125
+ out.push({
3126
+ method: field.toUpperCase(),
3127
+ pathTemplate: canonicalizeTemplate(stripChiRegex(prefix + leaf)),
3128
+ line: node.startPosition.row + 1,
3129
+ framework: "chi"
3130
+ });
3131
+ }
3132
+ return;
3133
+ }
3134
+ }
3135
+ }
3136
+ chiWalk(node, prefix, out);
3137
+ }
3138
+ function netHttpRoutesFromSource(source, parser) {
3139
+ const tree = parseSource2(parser, source);
3140
+ if (!goImportsNetHttp(tree.rootNode)) return [];
3141
+ const out = [];
3142
+ walk(tree.rootNode, (node) => {
3143
+ if (node.type !== "call_expression") return;
3144
+ const fn = node.childForFieldName("function");
3145
+ if (fn?.type !== "selector_expression") return;
3146
+ const field = fn.childForFieldName("field")?.text;
3147
+ if (field !== "HandleFunc" && field !== "Handle") return;
3148
+ const leaf = goStringLiteral(node.childForFieldName("arguments")?.namedChild(0));
3149
+ if (leaf === null) return;
3150
+ const sp = leaf.indexOf(" ");
3151
+ if (sp < 0) return;
3152
+ const method = leaf.slice(0, sp);
3153
+ const rest = leaf.slice(sp + 1);
3154
+ if (!NET_HTTP_METHODS.has(method)) return;
3155
+ if (!rest.startsWith("/")) return;
3156
+ out.push({
3157
+ method,
3158
+ pathTemplate: canonicalizeTemplate(rest),
3159
+ line: node.startPosition.row + 1,
3160
+ framework: "net/http"
3161
+ });
3162
+ });
3163
+ return out;
3164
+ }
3165
+ function goImportsNetHttp(root) {
3166
+ let found = false;
3167
+ walk(root, (node) => {
3168
+ if (found || node.type !== "import_spec") return;
3169
+ for (let i = 0; i < node.namedChildCount; i++) {
3170
+ const child = node.namedChild(i);
3171
+ if (goStringLiteral(child) === "net/http") found = true;
3172
+ }
3173
+ });
3174
+ return found;
3175
+ }
3064
3176
  var FASTAPI_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
3065
3177
  var NESTJS_METHODS = /* @__PURE__ */ new Map([
3066
3178
  ["Get", "GET"],
@@ -4326,9 +4438,11 @@ async function addRoutes(graph, services) {
4326
4438
  const hasGin = deps["github.com/gin-gonic/gin"] !== void 0;
4327
4439
  const hasEcho = deps["github.com/labstack/echo/v4"] !== void 0 || deps["github.com/labstack/echo"] !== void 0;
4328
4440
  const hasFiber = deps["github.com/gofiber/fiber/v2"] !== void 0 || deps["github.com/gofiber/fiber/v3"] !== void 0;
4441
+ const hasChi = deps["github.com/go-chi/chi/v5"] !== void 0 || deps["github.com/go-chi/chi"] !== void 0;
4442
+ const isGoService = service.node.language === "go";
4329
4443
  const hasRails = deps["rails"] !== void 0;
4330
4444
  const hasLaravel = deps["laravel/framework"] !== void 0;
4331
- if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasNestjs && !hasFastapi && !hasFlask && !hasDjango && !hasGin && !hasEcho && !hasFiber && !hasRails && !hasLaravel)
4445
+ if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasNestjs && !hasFastapi && !hasFlask && !hasDjango && !hasGin && !hasEcho && !hasFiber && !hasChi && !isGoService && !hasRails && !hasLaravel)
4332
4446
  continue;
4333
4447
  const files = await loadSourceFiles(service.dir);
4334
4448
  const mountPrefixes = hasExpress ? await expressMountPrefixes(files, service.dir, await loadTsPathConfig(service.dir)) : /* @__PURE__ */ new Map();
@@ -4355,7 +4469,9 @@ async function addRoutes(graph, services) {
4355
4469
  if (hasGin) routes = ginRoutesFromSource(file.content, goParser);
4356
4470
  else if (hasEcho) routes = echoRoutesFromSource(file.content, goParser);
4357
4471
  else if (hasFiber) routes = fiberRoutesFromSource(file.content, goParser);
4472
+ else if (hasChi) routes = chiRoutesFromSource(file.content, goParser);
4358
4473
  else routes = [];
4474
+ routes = routes.concat(netHttpRoutesFromSource(file.content, goParser));
4359
4475
  } else if (isPy) {
4360
4476
  routes = hasFastapi || hasFlask ? pythonRoutesFromSource(file.content, pyParser, hasFastapi ? "fastapi" : "flask") : [];
4361
4477
  if (hasDjango) routes = routes.concat(djangoRoutesFromSource(file.content, pyParser));
@@ -4728,10 +4844,15 @@ function resolveDistToSrc(absFilepath, line) {
4728
4844
  }
4729
4845
  if (!entry2) return null;
4730
4846
  try {
4731
- const pos = entry2.consumer.originalPositionFor({
4732
- line: line !== void 0 && Number.isFinite(line) ? line : 1,
4733
- column: 0
4734
- });
4847
+ const queryLine = line !== void 0 && Number.isFinite(line) ? line : 1;
4848
+ let pos = entry2.consumer.originalPositionFor({ line: queryLine, column: 0 });
4849
+ if (!pos || !pos.source) {
4850
+ pos = entry2.consumer.originalPositionFor({
4851
+ line: queryLine,
4852
+ column: 0,
4853
+ bias: sourceMapJs.SourceMapConsumer.LEAST_UPPER_BOUND
4854
+ });
4855
+ }
4735
4856
  if (!pos || !pos.source) return null;
4736
4857
  const root = entry2.consumer.sourceRoot ?? "";
4737
4858
  const resolved = import_node_path8.default.resolve(entry2.dir, root, pos.source);
@@ -4740,6 +4861,9 @@ function resolveDistToSrc(absFilepath, line) {
4740
4861
  return null;
4741
4862
  }
4742
4863
  }
4864
+ function hasAdjacentSourceMap(absFilepath) {
4865
+ return sourceMapCache.get(absFilepath) != null;
4866
+ }
4743
4867
  function callSiteFromSpan(span, serviceNode, scanPath) {
4744
4868
  const filepath = codeFilepathOf(span.attributes);
4745
4869
  if (filepath === void 0) return null;
@@ -4755,7 +4879,7 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
4755
4879
  }
4756
4880
  const relPath = relPathForRuntimeFile(effectivePath, serviceNode, scanPath);
4757
4881
  if (!relPath) return null;
4758
- if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && serviceNode?.name) {
4882
+ if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && !hasAdjacentSourceMap(abs) && serviceNode?.name) {
4759
4883
  warnNoSourceMaps(serviceNode.name);
4760
4884
  }
4761
4885
  const fn = codeFunctionOf(span.attributes);
@@ -5031,7 +5155,7 @@ function resolveServiceId(graph, host, env) {
5031
5155
  function frontierIdFor(host) {
5032
5156
  return (0, import_types8.frontierId)(host);
5033
5157
  }
5034
- function ensureServiceNode(graph, serviceName, env) {
5158
+ function resolveFusedServiceId(graph, serviceName, env) {
5035
5159
  const id = (0, import_types8.serviceId)(serviceName, env);
5036
5160
  if (graph.hasNode(id)) return id;
5037
5161
  const wanted = serviceName.toLowerCase();
@@ -5041,17 +5165,21 @@ function ensureServiceNode(graph, serviceName, env) {
5041
5165
  if (svc.discoveredVia === "otel") return false;
5042
5166
  return typeof svc.name === "string" && svc.name.toLowerCase() === wanted;
5043
5167
  });
5044
- if (extractedId) return extractedId;
5168
+ return extractedId ?? id;
5169
+ }
5170
+ function ensureServiceNode(graph, serviceName, env) {
5171
+ const resolved = resolveFusedServiceId(graph, serviceName, env);
5172
+ if (graph.hasNode(resolved)) return resolved;
5045
5173
  const node = {
5046
- id,
5174
+ id: resolved,
5047
5175
  type: import_types8.NodeType.ServiceNode,
5048
5176
  name: serviceName,
5049
5177
  language: "unknown",
5050
5178
  discoveredVia: "otel",
5051
5179
  ...env !== "unknown" ? { env } : {}
5052
5180
  };
5053
- graph.addNode(id, node);
5054
- return id;
5181
+ graph.addNode(resolved, node);
5182
+ return resolved;
5055
5183
  }
5056
5184
  function ensureInfraNode(graph, kind, name, provider) {
5057
5185
  const id = (0, import_types8.infraId)(kind, name);
@@ -5245,7 +5373,7 @@ async function appendErrorEvent(ctx, ev) {
5245
5373
  await import_node_fs7.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
5246
5374
  }
5247
5375
  function incidentAffectedNode(span, graph, scanPath) {
5248
- const sid = (0, import_types8.serviceId)(span.service, span.env);
5376
+ const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
5249
5377
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
5250
5378
  const callSite = callSiteFromSpan(span, serviceNode, scanPath);
5251
5379
  if (callSite) {
@@ -5966,6 +6094,7 @@ function goFramework(deps) {
5966
6094
  if (deps["github.com/gin-gonic/gin"]) return "gin";
5967
6095
  if (deps["github.com/labstack/echo/v4"] || deps["github.com/labstack/echo"]) return "echo";
5968
6096
  if (deps["github.com/gofiber/fiber/v2"] || deps["github.com/gofiber/fiber/v3"]) return "fiber";
6097
+ if (deps["github.com/go-chi/chi/v5"] || deps["github.com/go-chi/chi"]) return "chi";
5969
6098
  return void 0;
5970
6099
  }
5971
6100
  async function discoverGoService(scanPath, dir) {
@@ -6643,7 +6772,7 @@ function disambiguate(defs) {
6643
6772
  }
6644
6773
  async function addSymbols(graph, services) {
6645
6774
  const parsers = /* @__PURE__ */ new Map();
6646
- const parserForExt4 = (ext) => {
6775
+ const parserForExt5 = (ext) => {
6647
6776
  const grammar = GRAMMAR_BY_EXT[ext];
6648
6777
  if (!grammar) return null;
6649
6778
  let parser = parsers.get(ext);
@@ -6659,7 +6788,7 @@ async function addSymbols(graph, services) {
6659
6788
  for (const service of services) {
6660
6789
  const files = await loadSourceFiles(service.dir);
6661
6790
  for (const file of files) {
6662
- const parser = parserForExt4(import_node_path17.default.extname(file.path));
6791
+ const parser = parserForExt5(import_node_path17.default.extname(file.path));
6663
6792
  if (!parser) continue;
6664
6793
  const relPath = toPosix(import_node_path17.default.relative(service.dir, file.path));
6665
6794
  let defs;
@@ -6811,7 +6940,7 @@ function stringInner(node) {
6811
6940
  }
6812
6941
  async function addSymbolEdges(graph, services) {
6813
6942
  const parsers = /* @__PURE__ */ new Map();
6814
- const parserForExt4 = (ext) => {
6943
+ const parserForExt5 = (ext) => {
6815
6944
  const grammar = GRAMMAR_BY_EXT[ext];
6816
6945
  if (!grammar) return null;
6817
6946
  let parser = parsers.get(ext);
@@ -6828,7 +6957,7 @@ async function addSymbolEdges(graph, services) {
6828
6957
  const tsPaths = await loadTsPathConfig(service.dir);
6829
6958
  const files = await loadSourceFiles(service.dir);
6830
6959
  for (const file of files) {
6831
- const parser = parserForExt4(import_node_path18.default.extname(file.path));
6960
+ const parser = parserForExt5(import_node_path18.default.extname(file.path));
6832
6961
  if (!parser) continue;
6833
6962
  const relPath = toPosix(import_node_path18.default.relative(service.dir, file.path));
6834
6963
  const fileDir = import_node_path18.default.dirname(file.path);
@@ -7090,7 +7219,7 @@ function firstReferenceLines(root, wanted) {
7090
7219
  }
7091
7220
  async function addServerActions(graph, services) {
7092
7221
  const parsers = /* @__PURE__ */ new Map();
7093
- const parserForExt4 = (ext) => {
7222
+ const parserForExt5 = (ext) => {
7094
7223
  const grammar = GRAMMAR_BY_EXT[ext];
7095
7224
  if (!grammar) return null;
7096
7225
  let parser = parsers.get(ext);
@@ -7113,7 +7242,7 @@ async function addServerActions(graph, services) {
7113
7242
  const files = await loadSourceFiles(service.dir);
7114
7243
  for (const file of files) {
7115
7244
  if (isTestPath(file.path)) continue;
7116
- const parser = parserForExt4(import_node_path19.default.extname(file.path));
7245
+ const parser = parserForExt5(import_node_path19.default.extname(file.path));
7117
7246
  if (!parser) continue;
7118
7247
  const relPath = toPosix(import_node_path19.default.relative(service.dir, file.path));
7119
7248
  let root;
@@ -7176,7 +7305,7 @@ async function addServerActions(graph, services) {
7176
7305
  }
7177
7306
  for (const file of files) {
7178
7307
  if (isTestPath(file.path)) continue;
7179
- const parser = parserForExt4(import_node_path19.default.extname(file.path));
7308
+ const parser = parserForExt5(import_node_path19.default.extname(file.path));
7180
7309
  if (!parser) continue;
7181
7310
  const relPath = toPosix(import_node_path19.default.relative(service.dir, file.path));
7182
7311
  const fileDir = import_node_path19.default.dirname(file.path);
@@ -8140,6 +8269,7 @@ init_cjs_shims();
8140
8269
  var import_node_path30 = __toESM(require("path"), 1);
8141
8270
  var import_tree_sitter6 = __toESM(require("tree-sitter"), 1);
8142
8271
  var import_tree_sitter_javascript4 = __toESM(require("tree-sitter-javascript"), 1);
8272
+ var import_tree_sitter_typescript2 = __toESM(require("tree-sitter-typescript"), 1);
8143
8273
  var import_tree_sitter_python3 = __toESM(require("tree-sitter-python"), 1);
8144
8274
  var import_types20 = require("@neat.is/types");
8145
8275
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
@@ -8190,19 +8320,27 @@ function callsFromSource(source, parser, knownHosts) {
8190
8320
  }
8191
8321
  return out;
8192
8322
  }
8193
- function makeJsParser3() {
8194
- const p = new import_tree_sitter6.default();
8195
- p.setLanguage(import_tree_sitter_javascript4.default);
8196
- return p;
8197
- }
8198
- function makePyParser3() {
8199
- const p = new import_tree_sitter6.default();
8200
- p.setLanguage(import_tree_sitter_python3.default);
8201
- return p;
8323
+ var GRAMMAR_BY_EXT2 = {
8324
+ ".ts": import_tree_sitter_typescript2.default.typescript,
8325
+ ".tsx": import_tree_sitter_typescript2.default.tsx,
8326
+ ".js": import_tree_sitter_javascript4.default,
8327
+ ".jsx": import_tree_sitter_javascript4.default,
8328
+ ".mjs": import_tree_sitter_javascript4.default,
8329
+ ".cjs": import_tree_sitter_javascript4.default,
8330
+ ".py": import_tree_sitter_python3.default
8331
+ };
8332
+ function parserForExt(ext, cache) {
8333
+ const grammar = GRAMMAR_BY_EXT2[ext] ?? import_tree_sitter_javascript4.default;
8334
+ let parser = cache.get(grammar);
8335
+ if (!parser) {
8336
+ parser = new import_tree_sitter6.default();
8337
+ parser.setLanguage(grammar);
8338
+ cache.set(grammar, parser);
8339
+ }
8340
+ return parser;
8202
8341
  }
8203
8342
  async function addHttpCallEdges(graph, services) {
8204
- const jsParser = makeJsParser3();
8205
- const pyParser = makePyParser3();
8343
+ const parserCache = /* @__PURE__ */ new Map();
8206
8344
  const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
8207
8345
  let nodesAdded = 0;
8208
8346
  let edgesAdded = 0;
@@ -8211,7 +8349,7 @@ async function addHttpCallEdges(graph, services) {
8211
8349
  const seen = /* @__PURE__ */ new Set();
8212
8350
  for (const file of files) {
8213
8351
  if (isTestPath(file.path)) continue;
8214
- const parser = import_node_path30.default.extname(file.path) === ".py" ? pyParser : jsParser;
8352
+ const parser = parserForExt(import_node_path30.default.extname(file.path), parserCache);
8215
8353
  let sites;
8216
8354
  try {
8217
8355
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -8284,7 +8422,7 @@ function parseSource5(parser, source) {
8284
8422
  (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK5)
8285
8423
  );
8286
8424
  }
8287
- function makeJsParser4() {
8425
+ function makeJsParser3() {
8288
8426
  const p = new import_tree_sitter7.default();
8289
8427
  p.setLanguage(import_tree_sitter_javascript5.default);
8290
8428
  return p;
@@ -8462,7 +8600,7 @@ function findRoute(entries, method, normalizedPath) {
8462
8600
  );
8463
8601
  }
8464
8602
  async function addRouteCallEdges(graph, services) {
8465
- const jsParser = makeJsParser4();
8603
+ const jsParser = makeJsParser3();
8466
8604
  const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
8467
8605
  const routeIndex = buildRouteIndex(graph);
8468
8606
  if (routeIndex.size === 0) return { nodesAdded: 0, edgesAdded: 0 };
@@ -8854,7 +8992,7 @@ var import_tree_sitter_javascript6 = __toESM(require("tree-sitter-javascript"),
8854
8992
  var import_types27 = require("@neat.is/types");
8855
8993
  var FIRESTORE_CLIENT_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])firebase\/firestore['"`]/;
8856
8994
  var FIRESTORE_ADMIN_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])firebase-admin(?:\/firestore)?['"`]/;
8857
- function parserForExt(ext) {
8995
+ function parserForExt2(ext) {
8858
8996
  const p = new import_tree_sitter8.default();
8859
8997
  p.setLanguage(GRAMMAR_BY_EXT[ext] ?? import_tree_sitter_javascript6.default);
8860
8998
  return p;
@@ -9019,7 +9157,7 @@ function firestoreEndpointsFromFile(file, serviceDir) {
9019
9157
  const hasAdmin = FIRESTORE_ADMIN_IMPORT_RE.test(file.content);
9020
9158
  if (!hasClient && !hasAdmin) return [];
9021
9159
  const fileSdk = hasClient && !hasAdmin ? "client" : hasAdmin && !hasClient ? "admin" : null;
9022
- const tree = parseSource3(parserForExt(import_node_path37.default.extname(file.path)), file.content);
9160
+ const tree = parseSource3(parserForExt2(import_node_path37.default.extname(file.path)), file.content);
9023
9161
  const clientVars = firestoreClientVars(tree.rootNode);
9024
9162
  const collLine = /* @__PURE__ */ new Map();
9025
9163
  const writes = /* @__PURE__ */ new Map();
@@ -9436,7 +9574,7 @@ var import_tree_sitter_python4 = __toESM(require("tree-sitter-python"), 1);
9436
9574
  var import_types29 = require("@neat.is/types");
9437
9575
  var SQLALCHEMY_IMPORT_RE = /(?:from|import)\s+(?:flask_sqlalchemy|sqlalchemy)\b/;
9438
9576
  var PARSE_CHUNK6 = 16384;
9439
- function makePyParser4() {
9577
+ function makePyParser3() {
9440
9578
  const p = new import_tree_sitter9.default();
9441
9579
  p.setLanguage(import_tree_sitter_python4.default);
9442
9580
  return p;
@@ -9543,7 +9681,7 @@ function foreignKeyParentTable(call) {
9543
9681
  }
9544
9682
  function sqlalchemyForeignKeys(file, serviceDir) {
9545
9683
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
9546
- const tree = parseSource6(makePyParser4(), file.content);
9684
+ const tree = parseSource6(makePyParser3(), file.content);
9547
9685
  const out = [];
9548
9686
  const seen = /* @__PURE__ */ new Set();
9549
9687
  walk3(tree.rootNode, (node) => {
@@ -9580,7 +9718,7 @@ function sqlalchemyForeignKeys(file, serviceDir) {
9580
9718
  }
9581
9719
  function sqlalchemyEndpointsFromFile(file, serviceDir) {
9582
9720
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
9583
- const tree = parseSource6(makePyParser4(), file.content);
9721
+ const tree = parseSource6(makePyParser3(), file.content);
9584
9722
  const out = [];
9585
9723
  const seen = /* @__PURE__ */ new Set();
9586
9724
  const push = (name, line, columns) => {
@@ -9632,7 +9770,7 @@ function sqlalchemyEndpointsFromFile(file, serviceDir) {
9632
9770
  function buildSqlalchemyModelRegistry(files) {
9633
9771
  const table = /* @__PURE__ */ new Map();
9634
9772
  const ambiguous = /* @__PURE__ */ new Set();
9635
- const parser = makePyParser4();
9773
+ const parser = makePyParser3();
9636
9774
  for (const file of files) {
9637
9775
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) continue;
9638
9776
  const tree = parseSource6(parser, file.content);
@@ -9681,7 +9819,7 @@ function importsModelName(content, name) {
9681
9819
  function pythonOrmCrossFileEndpoints(files, serviceDir) {
9682
9820
  const registry = buildSqlalchemyModelRegistry(files);
9683
9821
  if (registry.size === 0) return [];
9684
- const parser = makePyParser4();
9822
+ const parser = makePyParser3();
9685
9823
  const out = [];
9686
9824
  const seen = /* @__PURE__ */ new Set();
9687
9825
  for (const file of files) {
@@ -9719,7 +9857,7 @@ var import_tree_sitter_python5 = __toESM(require("tree-sitter-python"), 1);
9719
9857
  var import_types30 = require("@neat.is/types");
9720
9858
  var DJANGO_IMPORT_RE = /(?:from|import)\s+django\b/;
9721
9859
  var PARSE_CHUNK7 = 16384;
9722
- function makePyParser5() {
9860
+ function makePyParser4() {
9723
9861
  const p = new import_tree_sitter10.default();
9724
9862
  p.setLanguage(import_tree_sitter_python5.default);
9725
9863
  return p;
@@ -9780,7 +9918,7 @@ function readMeta(body) {
9780
9918
  }
9781
9919
  function djangoOrmEndpointsFromFile(file, serviceDir) {
9782
9920
  if (!DJANGO_IMPORT_RE.test(file.content)) return [];
9783
- const tree = parseSource7(makePyParser5(), file.content);
9921
+ const tree = parseSource7(makePyParser4(), file.content);
9784
9922
  const out = [];
9785
9923
  const seen = /* @__PURE__ */ new Set();
9786
9924
  const defaultAppLabel = import_node_path40.default.basename(import_node_path40.default.dirname(file.path));
@@ -9815,7 +9953,7 @@ var import_tree_sitter_javascript7 = __toESM(require("tree-sitter-javascript"),
9815
9953
  var import_types31 = require("@neat.is/types");
9816
9954
  var DRIZZLE_IMPORT_RE = /drizzle-orm/;
9817
9955
  var TABLE_BUILDERS = /* @__PURE__ */ new Set(["pgTable", "mysqlTable", "sqliteTable"]);
9818
- function parserForExt2(ext) {
9956
+ function parserForExt3(ext) {
9819
9957
  const p = new import_tree_sitter11.default();
9820
9958
  p.setLanguage(GRAMMAR_BY_EXT[ext] ?? import_tree_sitter_javascript7.default);
9821
9959
  return p;
@@ -9887,7 +10025,7 @@ function columnsFromObject(obj) {
9887
10025
  }
9888
10026
  function drizzleEndpointsFromFile(file, serviceDir) {
9889
10027
  if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
9890
- const tree = parseSource3(parserForExt2(import_node_path41.default.extname(file.path)), file.content);
10028
+ const tree = parseSource3(parserForExt3(import_node_path41.default.extname(file.path)), file.content);
9891
10029
  const out = [];
9892
10030
  const seen = /* @__PURE__ */ new Set();
9893
10031
  const walk9 = (node) => {
@@ -9976,7 +10114,7 @@ function referencesTargetVar(call) {
9976
10114
  }
9977
10115
  function drizzleForeignKeys(file, serviceDir) {
9978
10116
  if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
9979
- const tree = parseSource3(parserForExt2(import_node_path41.default.extname(file.path)), file.content);
10117
+ const tree = parseSource3(parserForExt3(import_node_path41.default.extname(file.path)), file.content);
9980
10118
  const { tables, varToTable } = collectDrizzleTables(tree.rootNode);
9981
10119
  const out = [];
9982
10120
  const seen = /* @__PURE__ */ new Set();
@@ -12683,7 +12821,7 @@ var import_tree_sitter_javascript8 = __toESM(require("tree-sitter-javascript"),
12683
12821
  var import_types47 = require("@neat.is/types");
12684
12822
  var ZOD_IMPORT_RE = /\bzod\b/;
12685
12823
  var ZOD_OBJECTS = /* @__PURE__ */ new Set(["z", "zod"]);
12686
- function parserForExt3(ext) {
12824
+ function parserForExt4(ext) {
12687
12825
  const p = new import_tree_sitter16.default();
12688
12826
  p.setLanguage(GRAMMAR_BY_EXT[ext] ?? import_tree_sitter_javascript8.default);
12689
12827
  return p;
@@ -12772,7 +12910,7 @@ function topLevelSchemas(root) {
12772
12910
  }
12773
12911
  function zodShapesFromFile(file, serviceDir) {
12774
12912
  if (!ZOD_IMPORT_RE.test(file.content)) return [];
12775
- const tree = parseSource3(parserForExt3(import_node_path57.default.extname(file.path)), file.content);
12913
+ const tree = parseSource3(parserForExt4(import_node_path57.default.extname(file.path)), file.content);
12776
12914
  const out = [];
12777
12915
  const seen = /* @__PURE__ */ new Set();
12778
12916
  for (const { name, call } of topLevelSchemas(tree.rootNode)) {
@@ -18738,6 +18876,19 @@ async function startDaemon(opts = {}) {
18738
18876
  let otlpAddress = "";
18739
18877
  let daemonRecord = null;
18740
18878
  if (bind) {
18879
+ let bareSpanIsRoutable2 = function(serviceName) {
18880
+ if (singleProject) {
18881
+ const slot = slots.get(singleProject);
18882
+ if (!slot) {
18883
+ return !serviceName || serviceNameMatchesProject(serviceName, singleProject);
18884
+ }
18885
+ return spanBelongsToSingleProject(slot.graph, singleProject, serviceName);
18886
+ }
18887
+ const entries = [...slots.values()].map((s) => s.entry);
18888
+ const target = routeSpanToProject(serviceName, entries);
18889
+ return slots.has(target) || slots.has(DEFAULT_PROJECT);
18890
+ };
18891
+ var bareSpanIsRoutable = bareSpanIsRoutable2;
18741
18892
  const auth = readAuthEnv();
18742
18893
  const host = resolveHost(opts, Boolean(auth.authToken));
18743
18894
  const restPort = resolveRestPort(opts);
@@ -18835,6 +18986,20 @@ async function startDaemon(opts = {}) {
18835
18986
  const liveEntries = await listProjects().catch(() => []);
18836
18987
  let slot = slots.get(project);
18837
18988
  if (!slot) {
18989
+ if (singleProject && project === singleProject) {
18990
+ slot = await tryRecoverSlot({
18991
+ name: singleProject,
18992
+ path: singleProjectPath,
18993
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
18994
+ languages: [],
18995
+ status: "active"
18996
+ });
18997
+ if (!slot || slot.status !== "active") {
18998
+ warnDroppedSpan(singleProject, slot?.errorReason ?? "unknown");
18999
+ return null;
19000
+ }
19001
+ return slot;
19002
+ }
18838
19003
  await recordUnroutedSpan(serviceName, traceId);
18839
19004
  return null;
18840
19005
  }
@@ -18901,7 +19066,30 @@ async function startDaemon(opts = {}) {
18901
19066
  // host, rather than accepting it and dropping the batch. `slots` covers
18902
19067
  // active/recovering projects, `bootstrapStatus` the ones still
18903
19068
  // extracting; a foreign or wrong-cased project name matches neither.
18904
- isProjectRegistered: (project) => slots.has(project) || bootstrapStatus.has(project)
19069
+ // A single-project daemon owns exactly one project by definition, so its
19070
+ // own name always counts as registered even before loadAll populates the
19071
+ // slot — otherwise a scoped span arriving during cold-start would 404 and
19072
+ // be lost (OTLP does not retry 4xx). resolveSlotByName then builds it (#879).
19073
+ isProjectRegistered: (project) => slots.has(project) || bootstrapStatus.has(project) || singleProject !== void 0 && project === singleProject,
19074
+ // #881 — the bare `/v1/traces` route replies before the span is routed
19075
+ // (off the queue), so tell the receiver, per batch, how many spans will
19076
+ // land on no project. It keeps the 200 but reports those as
19077
+ // partialSuccess.rejectedSpans instead of an empty partialSuccess that an
19078
+ // exporter reads as full acceptance. Pure — the drop + unrouted-ledger
19079
+ // write still happen on the async onSpan path.
19080
+ classifyBareRoutability: (spans) => {
19081
+ let rejected = 0;
19082
+ for (const span of spans) {
19083
+ if (!bareSpanIsRoutable2(span.service)) rejected++;
19084
+ }
19085
+ if (rejected === 0) return { rejected: 0 };
19086
+ const noun = rejected === 1 ? "span" : "spans";
19087
+ const verb = rejected === 1 ? "was" : "were";
19088
+ return {
19089
+ rejected,
19090
+ 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.`
19091
+ };
19092
+ }
18905
19093
  });
18906
19094
  otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
18907
19095
  console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);