@neat.is/core 0.7.8 → 0.7.10

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.cjs CHANGED
@@ -503,13 +503,18 @@ function loadProtobufResponseEncoder() {
503
503
  );
504
504
  return exportTraceServiceResponseType;
505
505
  }
506
- function encodeProtobufResponseBody() {
507
- if (cachedProtobufResponseBody) return cachedProtobufResponseBody;
506
+ function encodeProtobufResponseBody(rejected, message) {
508
507
  const Type = loadProtobufResponseEncoder();
509
- const msg = Type.create({});
510
- const encoded = Type.encode(msg).finish();
511
- cachedProtobufResponseBody = Buffer.from(encoded);
512
- return cachedProtobufResponseBody;
508
+ if (!rejected) {
509
+ if (cachedProtobufResponseBody) return cachedProtobufResponseBody;
510
+ const msg2 = Type.create({});
511
+ cachedProtobufResponseBody = Buffer.from(Type.encode(msg2).finish());
512
+ return cachedProtobufResponseBody;
513
+ }
514
+ const msg = Type.fromObject({
515
+ partial_success: { rejected_spans: rejected, error_message: message ?? "" }
516
+ });
517
+ return Buffer.from(Type.encode(msg).finish());
513
518
  }
514
519
  async function decodeProtobufBody(buf) {
515
520
  const Type = loadProtobufDecoder();
@@ -624,6 +629,13 @@ async function buildOtelReceiver(opts) {
624
629
  }
625
630
  return reply.code(200).header("content-type", "application/json").send({ partialSuccess: {} });
626
631
  }
632
+ function sendOtlpPartial(reply, flavor, rejected, message) {
633
+ if (flavor === "protobuf") {
634
+ const buf = encodeProtobufResponseBody(rejected, message);
635
+ return reply.code(200).header("content-type", "application/x-protobuf").send(buf);
636
+ }
637
+ return reply.code(200).header("content-type", "application/json").send({ partialSuccess: { rejectedSpans: rejected, errorMessage: message } });
638
+ }
627
639
  app.addContentTypeParser(
628
640
  "application/x-protobuf",
629
641
  { parseAs: "buffer", bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024 },
@@ -651,6 +663,10 @@ async function buildOtelReceiver(opts) {
651
663
  }
652
664
  }
653
665
  enqueue(spans);
666
+ if (opts.classifyBareRoutability) {
667
+ const { rejected, message } = opts.classifyBareRoutability(spans);
668
+ if (rejected > 0) return sendOtlpPartial(reply, result.flavor, rejected, message);
669
+ }
654
670
  return sendOtlpSuccess(reply, result.flavor);
655
671
  });
656
672
  app.post("/projects/:project/v1/traces", async (req, reply) => {
@@ -2250,6 +2266,7 @@ var import_yaml = require("yaml");
2250
2266
  var import_types3 = require("@neat.is/types");
2251
2267
  var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go", ".rb", ".php"]);
2252
2268
  var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
2269
+ var JSON_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["app.json", "app.config.json", "eas.json"]);
2253
2270
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
2254
2271
  "node_modules",
2255
2272
  ".git",
@@ -2289,6 +2306,7 @@ async function isPythonVenvDir(dir) {
2289
2306
  function isConfigFile(name) {
2290
2307
  const ext = import_node_path3.default.extname(name);
2291
2308
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2309
+ if (JSON_CONFIG_FILENAMES.has(name)) return { match: true, fileType: "json" };
2292
2310
  if (name === ".env" || name.startsWith(".env.")) {
2293
2311
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
2294
2312
  if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
@@ -4865,10 +4883,15 @@ function resolveDistToSrc(absFilepath, line) {
4865
4883
  }
4866
4884
  if (!entry) return null;
4867
4885
  try {
4868
- const pos = entry.consumer.originalPositionFor({
4869
- line: line !== void 0 && Number.isFinite(line) ? line : 1,
4870
- column: 0
4871
- });
4886
+ const queryLine = line !== void 0 && Number.isFinite(line) ? line : 1;
4887
+ let pos = entry.consumer.originalPositionFor({ line: queryLine, column: 0 });
4888
+ if (!pos || !pos.source) {
4889
+ pos = entry.consumer.originalPositionFor({
4890
+ line: queryLine,
4891
+ column: 0,
4892
+ bias: sourceMapJs.SourceMapConsumer.LEAST_UPPER_BOUND
4893
+ });
4894
+ }
4872
4895
  if (!pos || !pos.source) return null;
4873
4896
  const root = entry.consumer.sourceRoot ?? "";
4874
4897
  const resolved = import_node_path8.default.resolve(entry.dir, root, pos.source);
@@ -4877,6 +4900,9 @@ function resolveDistToSrc(absFilepath, line) {
4877
4900
  return null;
4878
4901
  }
4879
4902
  }
4903
+ function hasAdjacentSourceMap(absFilepath) {
4904
+ return sourceMapCache.get(absFilepath) != null;
4905
+ }
4880
4906
  function callSiteFromSpan(span, serviceNode, scanPath) {
4881
4907
  const filepath = codeFilepathOf(span.attributes);
4882
4908
  if (filepath === void 0) return null;
@@ -4892,7 +4918,7 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
4892
4918
  }
4893
4919
  const relPath = relPathForRuntimeFile(effectivePath, serviceNode, scanPath);
4894
4920
  if (!relPath) return null;
4895
- if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && serviceNode?.name) {
4921
+ if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && !hasAdjacentSourceMap(abs) && serviceNode?.name) {
4896
4922
  warnNoSourceMaps(serviceNode.name);
4897
4923
  }
4898
4924
  const fn = codeFunctionOf(span.attributes);
@@ -5168,7 +5194,7 @@ function resolveServiceId(graph, host, env) {
5168
5194
  function frontierIdFor(host) {
5169
5195
  return (0, import_types8.frontierId)(host);
5170
5196
  }
5171
- function ensureServiceNode(graph, serviceName, env) {
5197
+ function resolveFusedServiceId(graph, serviceName, env) {
5172
5198
  const id = (0, import_types8.serviceId)(serviceName, env);
5173
5199
  if (graph.hasNode(id)) return id;
5174
5200
  const wanted = serviceName.toLowerCase();
@@ -5178,17 +5204,21 @@ function ensureServiceNode(graph, serviceName, env) {
5178
5204
  if (svc.discoveredVia === "otel") return false;
5179
5205
  return typeof svc.name === "string" && svc.name.toLowerCase() === wanted;
5180
5206
  });
5181
- if (extractedId) return extractedId;
5207
+ return extractedId ?? id;
5208
+ }
5209
+ function ensureServiceNode(graph, serviceName, env) {
5210
+ const resolved = resolveFusedServiceId(graph, serviceName, env);
5211
+ if (graph.hasNode(resolved)) return resolved;
5182
5212
  const node = {
5183
- id,
5213
+ id: resolved,
5184
5214
  type: import_types8.NodeType.ServiceNode,
5185
5215
  name: serviceName,
5186
5216
  language: "unknown",
5187
5217
  discoveredVia: "otel",
5188
5218
  ...env !== "unknown" ? { env } : {}
5189
5219
  };
5190
- graph.addNode(id, node);
5191
- return id;
5220
+ graph.addNode(resolved, node);
5221
+ return resolved;
5192
5222
  }
5193
5223
  function ensureInfraNode(graph, kind, name, provider) {
5194
5224
  const id = (0, import_types8.infraId)(kind, name);
@@ -5381,8 +5411,23 @@ async function appendErrorEvent(ctx, ev) {
5381
5411
  await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(ctx.errorsPath), { recursive: true });
5382
5412
  await import_node_fs7.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
5383
5413
  }
5414
+ async function appendConnectorIncident(errorsPath, input) {
5415
+ const ev = {
5416
+ id: input.id,
5417
+ timestamp: input.timestamp,
5418
+ service: input.service,
5419
+ traceId: input.id,
5420
+ spanId: input.id,
5421
+ errorType: input.errorType,
5422
+ errorMessage: input.errorMessage,
5423
+ ...input.attributes && Object.keys(input.attributes).length > 0 ? { attributes: input.attributes } : {},
5424
+ affectedNode: input.affectedNode
5425
+ };
5426
+ await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(errorsPath), { recursive: true });
5427
+ await import_node_fs7.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
5428
+ }
5384
5429
  function incidentAffectedNode(span, graph, scanPath) {
5385
- const sid = (0, import_types8.serviceId)(span.service, span.env);
5430
+ const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
5386
5431
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
5387
5432
  const callSite = callSiteFromSpan(span, serviceNode, scanPath);
5388
5433
  if (callSite) {
@@ -6784,7 +6829,7 @@ function disambiguate(defs) {
6784
6829
  }
6785
6830
  async function addSymbols(graph, services) {
6786
6831
  const parsers = /* @__PURE__ */ new Map();
6787
- const parserForExt4 = (ext) => {
6832
+ const parserForExt5 = (ext) => {
6788
6833
  const grammar = GRAMMAR_BY_EXT[ext];
6789
6834
  if (!grammar) return null;
6790
6835
  let parser = parsers.get(ext);
@@ -6800,7 +6845,7 @@ async function addSymbols(graph, services) {
6800
6845
  for (const service of services) {
6801
6846
  const files = await loadSourceFiles(service.dir);
6802
6847
  for (const file of files) {
6803
- const parser = parserForExt4(import_node_path17.default.extname(file.path));
6848
+ const parser = parserForExt5(import_node_path17.default.extname(file.path));
6804
6849
  if (!parser) continue;
6805
6850
  const relPath = toPosix(import_node_path17.default.relative(service.dir, file.path));
6806
6851
  let defs;
@@ -6952,7 +6997,7 @@ function stringInner(node) {
6952
6997
  }
6953
6998
  async function addSymbolEdges(graph, services) {
6954
6999
  const parsers = /* @__PURE__ */ new Map();
6955
- const parserForExt4 = (ext) => {
7000
+ const parserForExt5 = (ext) => {
6956
7001
  const grammar = GRAMMAR_BY_EXT[ext];
6957
7002
  if (!grammar) return null;
6958
7003
  let parser = parsers.get(ext);
@@ -6969,7 +7014,7 @@ async function addSymbolEdges(graph, services) {
6969
7014
  const tsPaths = await loadTsPathConfig(service.dir);
6970
7015
  const files = await loadSourceFiles(service.dir);
6971
7016
  for (const file of files) {
6972
- const parser = parserForExt4(import_node_path18.default.extname(file.path));
7017
+ const parser = parserForExt5(import_node_path18.default.extname(file.path));
6973
7018
  if (!parser) continue;
6974
7019
  const relPath = toPosix(import_node_path18.default.relative(service.dir, file.path));
6975
7020
  const fileDir = import_node_path18.default.dirname(file.path);
@@ -7231,7 +7276,7 @@ function firstReferenceLines(root, wanted) {
7231
7276
  }
7232
7277
  async function addServerActions(graph, services) {
7233
7278
  const parsers = /* @__PURE__ */ new Map();
7234
- const parserForExt4 = (ext) => {
7279
+ const parserForExt5 = (ext) => {
7235
7280
  const grammar = GRAMMAR_BY_EXT[ext];
7236
7281
  if (!grammar) return null;
7237
7282
  let parser = parsers.get(ext);
@@ -7254,7 +7299,7 @@ async function addServerActions(graph, services) {
7254
7299
  const files = await loadSourceFiles(service.dir);
7255
7300
  for (const file of files) {
7256
7301
  if (isTestPath(file.path)) continue;
7257
- const parser = parserForExt4(import_node_path19.default.extname(file.path));
7302
+ const parser = parserForExt5(import_node_path19.default.extname(file.path));
7258
7303
  if (!parser) continue;
7259
7304
  const relPath = toPosix(import_node_path19.default.relative(service.dir, file.path));
7260
7305
  let root;
@@ -7317,7 +7362,7 @@ async function addServerActions(graph, services) {
7317
7362
  }
7318
7363
  for (const file of files) {
7319
7364
  if (isTestPath(file.path)) continue;
7320
- const parser = parserForExt4(import_node_path19.default.extname(file.path));
7365
+ const parser = parserForExt5(import_node_path19.default.extname(file.path));
7321
7366
  if (!parser) continue;
7322
7367
  const relPath = toPosix(import_node_path19.default.relative(service.dir, file.path));
7323
7368
  const fileDir = import_node_path19.default.dirname(file.path);
@@ -8281,6 +8326,7 @@ init_cjs_shims();
8281
8326
  var import_node_path30 = __toESM(require("path"), 1);
8282
8327
  var import_tree_sitter6 = __toESM(require("tree-sitter"), 1);
8283
8328
  var import_tree_sitter_javascript4 = __toESM(require("tree-sitter-javascript"), 1);
8329
+ var import_tree_sitter_typescript2 = __toESM(require("tree-sitter-typescript"), 1);
8284
8330
  var import_tree_sitter_python3 = __toESM(require("tree-sitter-python"), 1);
8285
8331
  var import_types20 = require("@neat.is/types");
8286
8332
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
@@ -8331,19 +8377,27 @@ function callsFromSource(source, parser, knownHosts) {
8331
8377
  }
8332
8378
  return out;
8333
8379
  }
8334
- function makeJsParser3() {
8335
- const p = new import_tree_sitter6.default();
8336
- p.setLanguage(import_tree_sitter_javascript4.default);
8337
- return p;
8338
- }
8339
- function makePyParser3() {
8340
- const p = new import_tree_sitter6.default();
8341
- p.setLanguage(import_tree_sitter_python3.default);
8342
- return p;
8380
+ var GRAMMAR_BY_EXT2 = {
8381
+ ".ts": import_tree_sitter_typescript2.default.typescript,
8382
+ ".tsx": import_tree_sitter_typescript2.default.tsx,
8383
+ ".js": import_tree_sitter_javascript4.default,
8384
+ ".jsx": import_tree_sitter_javascript4.default,
8385
+ ".mjs": import_tree_sitter_javascript4.default,
8386
+ ".cjs": import_tree_sitter_javascript4.default,
8387
+ ".py": import_tree_sitter_python3.default
8388
+ };
8389
+ function parserForExt(ext, cache) {
8390
+ const grammar = GRAMMAR_BY_EXT2[ext] ?? import_tree_sitter_javascript4.default;
8391
+ let parser = cache.get(grammar);
8392
+ if (!parser) {
8393
+ parser = new import_tree_sitter6.default();
8394
+ parser.setLanguage(grammar);
8395
+ cache.set(grammar, parser);
8396
+ }
8397
+ return parser;
8343
8398
  }
8344
8399
  async function addHttpCallEdges(graph, services) {
8345
- const jsParser = makeJsParser3();
8346
- const pyParser = makePyParser3();
8400
+ const parserCache = /* @__PURE__ */ new Map();
8347
8401
  const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
8348
8402
  let nodesAdded = 0;
8349
8403
  let edgesAdded = 0;
@@ -8352,7 +8406,7 @@ async function addHttpCallEdges(graph, services) {
8352
8406
  const seen = /* @__PURE__ */ new Set();
8353
8407
  for (const file of files) {
8354
8408
  if (isTestPath(file.path)) continue;
8355
- const parser = import_node_path30.default.extname(file.path) === ".py" ? pyParser : jsParser;
8409
+ const parser = parserForExt(import_node_path30.default.extname(file.path), parserCache);
8356
8410
  let sites;
8357
8411
  try {
8358
8412
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -8425,7 +8479,7 @@ function parseSource5(parser, source) {
8425
8479
  (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK5)
8426
8480
  );
8427
8481
  }
8428
- function makeJsParser4() {
8482
+ function makeJsParser3() {
8429
8483
  const p = new import_tree_sitter7.default();
8430
8484
  p.setLanguage(import_tree_sitter_javascript5.default);
8431
8485
  return p;
@@ -8603,7 +8657,7 @@ function findRoute(entries, method, normalizedPath) {
8603
8657
  );
8604
8658
  }
8605
8659
  async function addRouteCallEdges(graph, services) {
8606
- const jsParser = makeJsParser4();
8660
+ const jsParser = makeJsParser3();
8607
8661
  const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
8608
8662
  const routeIndex = buildRouteIndex(graph);
8609
8663
  if (routeIndex.size === 0) return { nodesAdded: 0, edgesAdded: 0 };
@@ -8995,7 +9049,7 @@ var import_tree_sitter_javascript6 = __toESM(require("tree-sitter-javascript"),
8995
9049
  var import_types27 = require("@neat.is/types");
8996
9050
  var FIRESTORE_CLIENT_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])firebase\/firestore['"`]/;
8997
9051
  var FIRESTORE_ADMIN_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])firebase-admin(?:\/firestore)?['"`]/;
8998
- function parserForExt(ext) {
9052
+ function parserForExt2(ext) {
8999
9053
  const p = new import_tree_sitter8.default();
9000
9054
  p.setLanguage(GRAMMAR_BY_EXT[ext] ?? import_tree_sitter_javascript6.default);
9001
9055
  return p;
@@ -9160,7 +9214,7 @@ function firestoreEndpointsFromFile(file, serviceDir) {
9160
9214
  const hasAdmin = FIRESTORE_ADMIN_IMPORT_RE.test(file.content);
9161
9215
  if (!hasClient && !hasAdmin) return [];
9162
9216
  const fileSdk = hasClient && !hasAdmin ? "client" : hasAdmin && !hasClient ? "admin" : null;
9163
- const tree = parseSource3(parserForExt(import_node_path37.default.extname(file.path)), file.content);
9217
+ const tree = parseSource3(parserForExt2(import_node_path37.default.extname(file.path)), file.content);
9164
9218
  const clientVars = firestoreClientVars(tree.rootNode);
9165
9219
  const collLine = /* @__PURE__ */ new Map();
9166
9220
  const writes = /* @__PURE__ */ new Map();
@@ -9577,7 +9631,7 @@ var import_tree_sitter_python4 = __toESM(require("tree-sitter-python"), 1);
9577
9631
  var import_types29 = require("@neat.is/types");
9578
9632
  var SQLALCHEMY_IMPORT_RE = /(?:from|import)\s+(?:flask_sqlalchemy|sqlalchemy)\b/;
9579
9633
  var PARSE_CHUNK6 = 16384;
9580
- function makePyParser4() {
9634
+ function makePyParser3() {
9581
9635
  const p = new import_tree_sitter9.default();
9582
9636
  p.setLanguage(import_tree_sitter_python4.default);
9583
9637
  return p;
@@ -9684,7 +9738,7 @@ function foreignKeyParentTable(call) {
9684
9738
  }
9685
9739
  function sqlalchemyForeignKeys(file, serviceDir) {
9686
9740
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
9687
- const tree = parseSource6(makePyParser4(), file.content);
9741
+ const tree = parseSource6(makePyParser3(), file.content);
9688
9742
  const out = [];
9689
9743
  const seen = /* @__PURE__ */ new Set();
9690
9744
  walk3(tree.rootNode, (node) => {
@@ -9721,7 +9775,7 @@ function sqlalchemyForeignKeys(file, serviceDir) {
9721
9775
  }
9722
9776
  function sqlalchemyEndpointsFromFile(file, serviceDir) {
9723
9777
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
9724
- const tree = parseSource6(makePyParser4(), file.content);
9778
+ const tree = parseSource6(makePyParser3(), file.content);
9725
9779
  const out = [];
9726
9780
  const seen = /* @__PURE__ */ new Set();
9727
9781
  const push = (name, line, columns) => {
@@ -9773,7 +9827,7 @@ function sqlalchemyEndpointsFromFile(file, serviceDir) {
9773
9827
  function buildSqlalchemyModelRegistry(files) {
9774
9828
  const table = /* @__PURE__ */ new Map();
9775
9829
  const ambiguous = /* @__PURE__ */ new Set();
9776
- const parser = makePyParser4();
9830
+ const parser = makePyParser3();
9777
9831
  for (const file of files) {
9778
9832
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) continue;
9779
9833
  const tree = parseSource6(parser, file.content);
@@ -9822,7 +9876,7 @@ function importsModelName(content, name) {
9822
9876
  function pythonOrmCrossFileEndpoints(files, serviceDir) {
9823
9877
  const registry = buildSqlalchemyModelRegistry(files);
9824
9878
  if (registry.size === 0) return [];
9825
- const parser = makePyParser4();
9879
+ const parser = makePyParser3();
9826
9880
  const out = [];
9827
9881
  const seen = /* @__PURE__ */ new Set();
9828
9882
  for (const file of files) {
@@ -9860,7 +9914,7 @@ var import_tree_sitter_python5 = __toESM(require("tree-sitter-python"), 1);
9860
9914
  var import_types30 = require("@neat.is/types");
9861
9915
  var DJANGO_IMPORT_RE = /(?:from|import)\s+django\b/;
9862
9916
  var PARSE_CHUNK7 = 16384;
9863
- function makePyParser5() {
9917
+ function makePyParser4() {
9864
9918
  const p = new import_tree_sitter10.default();
9865
9919
  p.setLanguage(import_tree_sitter_python5.default);
9866
9920
  return p;
@@ -9921,7 +9975,7 @@ function readMeta(body) {
9921
9975
  }
9922
9976
  function djangoOrmEndpointsFromFile(file, serviceDir) {
9923
9977
  if (!DJANGO_IMPORT_RE.test(file.content)) return [];
9924
- const tree = parseSource7(makePyParser5(), file.content);
9978
+ const tree = parseSource7(makePyParser4(), file.content);
9925
9979
  const out = [];
9926
9980
  const seen = /* @__PURE__ */ new Set();
9927
9981
  const defaultAppLabel = import_node_path40.default.basename(import_node_path40.default.dirname(file.path));
@@ -9956,7 +10010,7 @@ var import_tree_sitter_javascript7 = __toESM(require("tree-sitter-javascript"),
9956
10010
  var import_types31 = require("@neat.is/types");
9957
10011
  var DRIZZLE_IMPORT_RE = /drizzle-orm/;
9958
10012
  var TABLE_BUILDERS = /* @__PURE__ */ new Set(["pgTable", "mysqlTable", "sqliteTable"]);
9959
- function parserForExt2(ext) {
10013
+ function parserForExt3(ext) {
9960
10014
  const p = new import_tree_sitter11.default();
9961
10015
  p.setLanguage(GRAMMAR_BY_EXT[ext] ?? import_tree_sitter_javascript7.default);
9962
10016
  return p;
@@ -10028,7 +10082,7 @@ function columnsFromObject(obj) {
10028
10082
  }
10029
10083
  function drizzleEndpointsFromFile(file, serviceDir) {
10030
10084
  if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
10031
- const tree = parseSource3(parserForExt2(import_node_path41.default.extname(file.path)), file.content);
10085
+ const tree = parseSource3(parserForExt3(import_node_path41.default.extname(file.path)), file.content);
10032
10086
  const out = [];
10033
10087
  const seen = /* @__PURE__ */ new Set();
10034
10088
  const walk9 = (node) => {
@@ -10117,7 +10171,7 @@ function referencesTargetVar(call) {
10117
10171
  }
10118
10172
  function drizzleForeignKeys(file, serviceDir) {
10119
10173
  if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
10120
- const tree = parseSource3(parserForExt2(import_node_path41.default.extname(file.path)), file.content);
10174
+ const tree = parseSource3(parserForExt3(import_node_path41.default.extname(file.path)), file.content);
10121
10175
  const { tables, varToTable } = collectDrizzleTables(tree.rootNode);
10122
10176
  const out = [];
10123
10177
  const seen = /* @__PURE__ */ new Set();
@@ -11185,8 +11239,41 @@ var import_tree_sitter14 = __toESM(require("tree-sitter"), 1);
11185
11239
  var import_tree_sitter_go3 = __toESM(require("tree-sitter-go"), 1);
11186
11240
  var import_types35 = require("@neat.is/types");
11187
11241
  init_otel();
11188
- var SQL_METHODS = /* @__PURE__ */ new Set(["Exec", "ExecContext", "Query", "QueryContext", "QueryRow", "QueryRowContext"]);
11189
11242
  var PARSE_CHUNK10 = 16384;
11243
+ var DATABASE_SQL_METHODS = /* @__PURE__ */ new Set([
11244
+ "Query",
11245
+ "QueryContext",
11246
+ "QueryRow",
11247
+ "QueryRowContext",
11248
+ "Exec",
11249
+ "ExecContext",
11250
+ "Prepare",
11251
+ "PrepareContext"
11252
+ ]);
11253
+ var SQLX_METHODS = /* @__PURE__ */ new Set([
11254
+ "Get",
11255
+ "Select",
11256
+ "Queryx",
11257
+ "QueryRowx",
11258
+ "NamedExec",
11259
+ "NamedQuery",
11260
+ "MustExec",
11261
+ "Preparex",
11262
+ "GetContext",
11263
+ "SelectContext"
11264
+ ]);
11265
+ var DATABASE_SQL_IMPORT = "database/sql";
11266
+ var SQLX_IMPORT = "github.com/jmoiron/sqlx";
11267
+ function makeGoParser3() {
11268
+ const p = new import_tree_sitter14.default();
11269
+ p.setLanguage(import_tree_sitter_go3.default);
11270
+ return p;
11271
+ }
11272
+ function parseSource10(parser, source) {
11273
+ return parser.parse(
11274
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK10)
11275
+ );
11276
+ }
11190
11277
  function walk7(node, visit) {
11191
11278
  visit(node);
11192
11279
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -11194,25 +11281,54 @@ function walk7(node, visit) {
11194
11281
  if (child) walk7(child, visit);
11195
11282
  }
11196
11283
  }
11284
+ function goStringLiteralValue(node) {
11285
+ if (!node) return null;
11286
+ if (node.type === "interpreted_string_literal" || node.type === "raw_string_literal") {
11287
+ const t = node.text;
11288
+ return t.length >= 2 ? t.slice(1, -1) : "";
11289
+ }
11290
+ return null;
11291
+ }
11292
+ function goImportsAny(root, names) {
11293
+ let found = false;
11294
+ walk7(root, (node) => {
11295
+ if (found || node.type !== "import_spec") return;
11296
+ for (let i = 0; i < node.namedChildCount; i++) {
11297
+ const value = goStringLiteralValue(node.namedChild(i));
11298
+ if (value !== null && names.has(value)) found = true;
11299
+ }
11300
+ });
11301
+ return found;
11302
+ }
11303
+ function firstStringLiteralArg(argsNode) {
11304
+ if (!argsNode) return null;
11305
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
11306
+ const value = goStringLiteralValue(argsNode.namedChild(i));
11307
+ if (value !== null) return value;
11308
+ }
11309
+ return null;
11310
+ }
11197
11311
  function goSqlEndpointsFromFile(file, serviceDir) {
11198
11312
  if (import_node_path47.default.extname(file.path) !== ".go") return [];
11199
- const parser = new import_tree_sitter14.default();
11200
- parser.setLanguage(import_tree_sitter_go3.default);
11201
- const tree = parser.parse(
11202
- (index) => index >= file.content.length ? "" : file.content.slice(index, index + PARSE_CHUNK10)
11203
- );
11313
+ if (!file.content.includes(DATABASE_SQL_IMPORT) && !file.content.includes(SQLX_IMPORT)) return [];
11314
+ const tree = parseSource10(makeGoParser3(), file.content);
11315
+ const importsDatabaseSql = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([DATABASE_SQL_IMPORT]));
11316
+ const importsSqlx = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([SQLX_IMPORT]));
11317
+ if (!importsDatabaseSql && !importsSqlx) return [];
11204
11318
  const out = [];
11205
11319
  walk7(tree.rootNode, (node) => {
11206
11320
  if (node.type !== "call_expression") return;
11207
11321
  const fn = node.childForFieldName("function");
11208
11322
  if (fn?.type !== "selector_expression") return;
11209
11323
  const method = fn.childForFieldName("field")?.text;
11210
- if (!method || !SQL_METHODS.has(method)) return;
11211
- const arg = node.childForFieldName("arguments")?.namedChild(0);
11212
- if (!arg || arg.type !== "interpreted_string_literal" && arg.type !== "raw_string_literal") return;
11213
- const sql = arg.text.slice(1, -1);
11324
+ if (!method) return;
11325
+ const recognized = DATABASE_SQL_METHODS.has(method) || importsSqlx && SQLX_METHODS.has(method);
11326
+ if (!recognized) return;
11327
+ const sql = firstStringLiteralArg(node.childForFieldName("arguments"));
11328
+ if (sql === null) return;
11214
11329
  const table = tableFromSqlStatement(sql);
11215
11330
  if (!table) return;
11331
+ const columns = columnsFromSqlStatement(sql);
11216
11332
  const line = node.startPosition.row + 1;
11217
11333
  out.push({
11218
11334
  infraId: (0, import_types35.infraId)("sql-table", table),
@@ -11220,7 +11336,12 @@ function goSqlEndpointsFromFile(file, serviceDir) {
11220
11336
  kind: "sql-table",
11221
11337
  edgeType: "CALLS",
11222
11338
  confidenceKind: "verified-call-site",
11223
- evidence: { file: toPosix(import_node_path47.default.relative(serviceDir, file.path)), line, snippet: snippet(file.content, line) }
11339
+ ...columns.length > 0 ? { columns } : {},
11340
+ evidence: {
11341
+ file: toPosix(import_node_path47.default.relative(serviceDir, file.path)),
11342
+ line,
11343
+ snippet: snippet(file.content, line)
11344
+ }
11224
11345
  });
11225
11346
  });
11226
11347
  return out;
@@ -11234,12 +11355,12 @@ var import_tree_sitter_go4 = __toESM(require("tree-sitter-go"), 1);
11234
11355
  var import_types36 = require("@neat.is/types");
11235
11356
  var GORM_IMPORT_RE = /gorm\.io\/gorm/;
11236
11357
  var PARSE_CHUNK11 = 16384;
11237
- function makeGoParser3() {
11358
+ function makeGoParser4() {
11238
11359
  const p = new import_tree_sitter15.default();
11239
11360
  p.setLanguage(import_tree_sitter_go4.default);
11240
11361
  return p;
11241
11362
  }
11242
- function parseSource10(parser, source) {
11363
+ function parseSource11(parser, source) {
11243
11364
  return parser.parse(
11244
11365
  (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK11)
11245
11366
  );
@@ -11663,7 +11784,7 @@ function collectColumns(struct, structs, seen, prefix, out, emitted) {
11663
11784
  function gormEndpointsFromFile(file, serviceDir) {
11664
11785
  if (import_node_path48.default.extname(file.path) !== ".go") return [];
11665
11786
  if (!GORM_IMPORT_RE.test(file.content)) return [];
11666
- const tree = parseSource10(makeGoParser3(), file.content);
11787
+ const tree = parseSource11(makeGoParser4(), file.content);
11667
11788
  const { structs, models, tableFor } = analyze(tree);
11668
11789
  const out = [];
11669
11790
  const seenTables = /* @__PURE__ */ new Set();
@@ -11694,7 +11815,7 @@ function gormEndpointsFromFile(file, serviceDir) {
11694
11815
  function gormForeignKeys(file, serviceDir) {
11695
11816
  if (import_node_path48.default.extname(file.path) !== ".go") return [];
11696
11817
  if (!GORM_IMPORT_RE.test(file.content)) return [];
11697
- const tree = parseSource10(makeGoParser3(), file.content);
11818
+ const tree = parseSource11(makeGoParser4(), file.content);
11698
11819
  const { structs, models, tableFor } = analyze(tree);
11699
11820
  const out = [];
11700
11821
  const seen = /* @__PURE__ */ new Set();
@@ -12824,7 +12945,7 @@ var import_tree_sitter_javascript8 = __toESM(require("tree-sitter-javascript"),
12824
12945
  var import_types47 = require("@neat.is/types");
12825
12946
  var ZOD_IMPORT_RE = /\bzod\b/;
12826
12947
  var ZOD_OBJECTS = /* @__PURE__ */ new Set(["z", "zod"]);
12827
- function parserForExt3(ext) {
12948
+ function parserForExt4(ext) {
12828
12949
  const p = new import_tree_sitter16.default();
12829
12950
  p.setLanguage(GRAMMAR_BY_EXT[ext] ?? import_tree_sitter_javascript8.default);
12830
12951
  return p;
@@ -12913,7 +13034,7 @@ function topLevelSchemas(root) {
12913
13034
  }
12914
13035
  function zodShapesFromFile(file, serviceDir) {
12915
13036
  if (!ZOD_IMPORT_RE.test(file.content)) return [];
12916
- const tree = parseSource3(parserForExt3(import_node_path57.default.extname(file.path)), file.content);
13037
+ const tree = parseSource3(parserForExt4(import_node_path57.default.extname(file.path)), file.content);
12917
13038
  const out = [];
12918
13039
  const seen = /* @__PURE__ */ new Set();
12919
13040
  for (const { name, call } of topLevelSchemas(tree.rootNode)) {
@@ -13437,7 +13558,7 @@ function startPersistLoop(graph, outPath, opts = {}) {
13437
13558
  init_cjs_shims();
13438
13559
  var import_fastify2 = __toESM(require("fastify"), 1);
13439
13560
  var import_cors = __toESM(require("@fastify/cors"), 1);
13440
- var import_types80 = require("@neat.is/types");
13561
+ var import_types85 = require("@neat.is/types");
13441
13562
 
13442
13563
  // src/extend/index.ts
13443
13564
  init_cjs_shims();
@@ -14943,6 +15064,23 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
14943
15064
  unresolved++;
14944
15065
  continue;
14945
15066
  }
15067
+ if (signal.incident) {
15068
+ ensureServiceNode(graph, resolved.serviceName, NO_ENV);
15069
+ if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
15070
+ unresolved++;
15071
+ continue;
15072
+ }
15073
+ await appendConnectorIncident(ctx.errorsPath, {
15074
+ id: signal.incident.id,
15075
+ timestamp: signal.incident.timestamp,
15076
+ service: signal.incident.service,
15077
+ errorType: signal.incident.errorType,
15078
+ errorMessage: signal.incident.errorMessage,
15079
+ ...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
15080
+ affectedNode: resolved.targetNodeId
15081
+ });
15082
+ continue;
15083
+ }
14946
15084
  if (resolved.ensureInfraNode) {
14947
15085
  const { kind, name, provider } = resolved.ensureInfraNode;
14948
15086
  ensureInfraNode(graph, kind, name, provider);
@@ -17177,6 +17315,338 @@ function createPlanetscaleConnector(graph, config, deps = {}) {
17177
17315
  };
17178
17316
  }
17179
17317
 
17318
+ // src/connectors/eas/index.ts
17319
+ init_cjs_shims();
17320
+
17321
+ // src/connectors/eas/client.ts
17322
+ init_cjs_shims();
17323
+
17324
+ // src/connectors/eas/types.ts
17325
+ init_cjs_shims();
17326
+ function readEasCredentials(raw) {
17327
+ const token = raw["token"];
17328
+ if (typeof token !== "string" || token.length === 0) {
17329
+ throw new Error("eas connector: credentials.token (EXPO_TOKEN) must be a non-empty string");
17330
+ }
17331
+ return { token };
17332
+ }
17333
+ var EAS_STATUS_ERRORED = "ERRORED";
17334
+ var TRANSIENT_BUILD_PHASES = /* @__PURE__ */ new Set([
17335
+ "SPIN_UP_BUILDER",
17336
+ "PREPARE_CREDENTIALS",
17337
+ "RESTORE_CACHE",
17338
+ "UPLOAD_APPLICATION_ARCHIVE"
17339
+ ]);
17340
+ var INTERNAL_ERROR_CODE = /INTERNAL_SERVER_ERROR|EAS_BUILD_.*INTERNAL|_INTERNAL_ERROR|UNKNOWN_ERROR/i;
17341
+ function isTransientFailure(err) {
17342
+ if (!err) return false;
17343
+ const phase = typeof err.buildPhase === "string" ? err.buildPhase : void 0;
17344
+ if (phase) {
17345
+ if (TRANSIENT_BUILD_PHASES.has(phase)) return true;
17346
+ if (/^SPIN_UP|_CREDENTIALS$|^RESTORE_CACHE/i.test(phase)) return true;
17347
+ }
17348
+ const code = typeof err.errorCode === "string" ? err.errorCode : void 0;
17349
+ if (code && INTERNAL_ERROR_CODE.test(code)) return true;
17350
+ return false;
17351
+ }
17352
+ var FIELD_SEP3 = "\0";
17353
+ var EAS_TARGET_KIND = "eas-build";
17354
+ function packEasTargetName(identity) {
17355
+ return [identity.serviceName, identity.phase].join(FIELD_SEP3);
17356
+ }
17357
+ function parseEasTargetName(targetName) {
17358
+ const sep = targetName.indexOf(FIELD_SEP3);
17359
+ if (sep === -1) return null;
17360
+ const serviceName = targetName.slice(0, sep);
17361
+ const phase = targetName.slice(sep + 1);
17362
+ if (!serviceName) return null;
17363
+ return { serviceName, phase };
17364
+ }
17365
+
17366
+ // src/connectors/eas/client.ts
17367
+ var DEFAULT_EAS_API_URL = "https://api.expo.dev/graphql";
17368
+ var DEFAULT_PAGE_SIZE = 50;
17369
+ var DEFAULT_MAX_PAGES = 10;
17370
+ var DEFAULT_MAX_LOG_BYTES = 16 * 1024;
17371
+ var DEFAULT_MAX_LOOKBACK_MS6 = 7 * 24 * 60 * 60 * 1e3;
17372
+ var BUILDS_QUERY = `
17373
+ query NeatEasErroredBuilds($appId: String!, $offset: Int!, $limit: Int!) {
17374
+ app {
17375
+ byId(appId: $appId) {
17376
+ builds(offset: $offset, limit: $limit, filter: { status: ERRORED }) {
17377
+ id
17378
+ status
17379
+ platform
17380
+ buildProfile
17381
+ gitCommitHash
17382
+ gitCommitMessage
17383
+ gitRef
17384
+ isGitWorkingTreeDirty
17385
+ createdAt
17386
+ completedAt
17387
+ error {
17388
+ buildPhase
17389
+ errorCode
17390
+ message
17391
+ docsUrl
17392
+ }
17393
+ logFileUrls
17394
+ }
17395
+ }
17396
+ }
17397
+ }
17398
+ `;
17399
+ async function easGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
17400
+ const res = await junctionFetch(
17401
+ apiUrl,
17402
+ {
17403
+ method: "POST",
17404
+ headers: {
17405
+ "Content-Type": "application/json",
17406
+ ...bearerAuthHeader(token)
17407
+ },
17408
+ body: JSON.stringify({ query, variables })
17409
+ },
17410
+ // accountKey: the Expo app id — the per-(provider, accountKey) rate-limit
17411
+ // bucket (ADR-131), the closest thing this connector carries to "one account".
17412
+ { provider: "eas", accountKey, ...fetchImpl ? { fetchImpl } : {} }
17413
+ );
17414
+ if (!res.ok) {
17415
+ throw new Error(`Expo GraphQL request failed: ${res.status} ${res.statusText}`);
17416
+ }
17417
+ const body = await res.json();
17418
+ if (body.errors && body.errors.length > 0) {
17419
+ throw new Error(`Expo GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
17420
+ }
17421
+ if (!body.data) throw new Error("Expo GraphQL response carried no data");
17422
+ return body.data;
17423
+ }
17424
+ async function fetchErroredBuilds(token, config, fetchImpl) {
17425
+ const apiUrl = config.apiUrl ?? DEFAULT_EAS_API_URL;
17426
+ const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
17427
+ const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
17428
+ const out = [];
17429
+ const seen = /* @__PURE__ */ new Set();
17430
+ for (let page = 0; page < maxPages; page++) {
17431
+ const data = await easGraphQL(
17432
+ apiUrl,
17433
+ token,
17434
+ BUILDS_QUERY,
17435
+ { appId: config.appId, offset: page * pageSize, limit: pageSize },
17436
+ config.appId,
17437
+ fetchImpl
17438
+ );
17439
+ const builds = data.app?.byId?.builds;
17440
+ if (!Array.isArray(builds)) break;
17441
+ let added = 0;
17442
+ for (const b of builds) {
17443
+ if (!b || typeof b.id !== "string" || b.id.length === 0) continue;
17444
+ if (b.status !== EAS_STATUS_ERRORED) continue;
17445
+ if (seen.has(b.id)) continue;
17446
+ seen.add(b.id);
17447
+ out.push(b);
17448
+ added++;
17449
+ }
17450
+ if (builds.length < pageSize) break;
17451
+ if (added === 0) break;
17452
+ }
17453
+ return out;
17454
+ }
17455
+ async function fetchBuildLogs(logFileUrls, maxBytes = DEFAULT_MAX_LOG_BYTES, fetchImpl) {
17456
+ if (!Array.isArray(logFileUrls) || logFileUrls.length === 0) return "";
17457
+ const doFetch = fetchImpl ?? fetch;
17458
+ const chunks = [];
17459
+ for (const url of logFileUrls) {
17460
+ if (typeof url !== "string" || url.length === 0) continue;
17461
+ try {
17462
+ const res = await doFetch(url);
17463
+ if (!res.ok) continue;
17464
+ chunks.push(await res.text());
17465
+ } catch {
17466
+ }
17467
+ }
17468
+ const joined = chunks.join("\n");
17469
+ return joined.length > maxBytes ? joined.slice(joined.length - maxBytes) : joined;
17470
+ }
17471
+
17472
+ // src/connectors/eas/map.ts
17473
+ init_cjs_shims();
17474
+ function buildEventTime(build) {
17475
+ if (typeof build.completedAt === "string" && build.completedAt.length > 0) return build.completedAt;
17476
+ if (typeof build.createdAt === "string" && build.createdAt.length > 0) return build.createdAt;
17477
+ return (/* @__PURE__ */ new Date()).toISOString();
17478
+ }
17479
+ function incidentMessage2(build) {
17480
+ const err = build.error ?? {};
17481
+ const phase = typeof err.buildPhase === "string" && err.buildPhase.length > 0 ? ` at ${err.buildPhase}` : "";
17482
+ const detail = typeof err.message === "string" && err.message.trim().length > 0 && err.message.trim() || typeof err.errorCode === "string" && err.errorCode.length > 0 && err.errorCode || "no error detail reported";
17483
+ let msg = `EAS build failed${phase}: ${detail}`;
17484
+ if (build.isGitWorkingTreeDirty === true) {
17485
+ msg += " (built from a dirty working tree \u2014 the commit may not represent what built)";
17486
+ }
17487
+ return msg;
17488
+ }
17489
+ function incidentAttributes(build) {
17490
+ const attrs = {};
17491
+ const err = build.error ?? {};
17492
+ const put = (k, v) => {
17493
+ if (typeof v === "string" && v.length === 0) return;
17494
+ if (v !== void 0 && v !== null) attrs[k] = v;
17495
+ };
17496
+ put("eas.buildId", build.id);
17497
+ put("eas.platform", build.platform ?? void 0);
17498
+ put("eas.buildProfile", build.buildProfile ?? void 0);
17499
+ put("eas.buildPhase", err.buildPhase ?? void 0);
17500
+ put("eas.errorCode", err.errorCode ?? void 0);
17501
+ put("eas.docsUrl", err.docsUrl ?? void 0);
17502
+ put("eas.gitCommitHash", build.gitCommitHash ?? void 0);
17503
+ put("eas.gitRef", build.gitRef ?? void 0);
17504
+ put("eas.gitCommitMessage", build.gitCommitMessage ?? void 0);
17505
+ if (typeof build.isGitWorkingTreeDirty === "boolean") {
17506
+ attrs["eas.gitWorkingTreeDirty"] = build.isGitWorkingTreeDirty;
17507
+ if (build.isGitWorkingTreeDirty) attrs["eas.confidence"] = "low";
17508
+ }
17509
+ put("eas.createdAt", build.createdAt ?? void 0);
17510
+ put("eas.completedAt", build.completedAt ?? void 0);
17511
+ if (typeof build.logsText === "string" && build.logsText.length > 0) {
17512
+ attrs["eas.logs"] = build.logsText;
17513
+ }
17514
+ return attrs;
17515
+ }
17516
+ function mapBuildToSignal(build, serviceName) {
17517
+ if (!build || typeof build !== "object") return null;
17518
+ if (build.status !== EAS_STATUS_ERRORED) return null;
17519
+ if (!build.error) return null;
17520
+ if (isTransientFailure(build.error)) return null;
17521
+ const timestamp = buildEventTime(build);
17522
+ const phase = typeof build.error.buildPhase === "string" ? build.error.buildPhase : "";
17523
+ return {
17524
+ targetKind: EAS_TARGET_KIND,
17525
+ targetName: packEasTargetName({ serviceName, phase }),
17526
+ // Incident-only — no edge, so no call/error count to replay.
17527
+ callCount: 0,
17528
+ errorCount: 0,
17529
+ lastObservedIso: timestamp,
17530
+ incident: {
17531
+ id: `eas:build:${build.id}`,
17532
+ timestamp,
17533
+ service: serviceName,
17534
+ errorType: "eas-build-failure",
17535
+ errorMessage: incidentMessage2(build),
17536
+ attributes: incidentAttributes(build)
17537
+ }
17538
+ };
17539
+ }
17540
+ function mapBuildsToSignals(builds, serviceName) {
17541
+ const out = [];
17542
+ for (const build of builds) {
17543
+ const signal = mapBuildToSignal(build, serviceName);
17544
+ if (signal) out.push(signal);
17545
+ }
17546
+ return out;
17547
+ }
17548
+
17549
+ // src/connectors/eas/resolve.ts
17550
+ init_cjs_shims();
17551
+ var import_types82 = require("@neat.is/types");
17552
+ var NO_ENV2 = "unknown";
17553
+ var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
17554
+ var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
17555
+ "READ_APP_CONFIG",
17556
+ "CONFIGURE_EXPO_UPDATES",
17557
+ "CALCULATE_EXPO_UPDATES_RUNTIME_VERSION"
17558
+ ]);
17559
+ function configBasenamesForPhase(phase) {
17560
+ if (EAS_JSON_PHASES.has(phase)) return ["eas.json"];
17561
+ if (APP_CONFIG_PHASES.has(phase)) return ["app.json", "app.config.json"];
17562
+ return [];
17563
+ }
17564
+ function configNodeService(graph, configNodeId) {
17565
+ for (const edgeId of graph.inboundEdges(configNodeId)) {
17566
+ const edge = graph.getEdgeAttributes(edgeId);
17567
+ if (edge.type !== import_types82.EdgeType.CONFIGURED_BY) continue;
17568
+ const parsed = (0, import_types82.parseFileId)(edge.source);
17569
+ if (parsed) return parsed.service;
17570
+ }
17571
+ return null;
17572
+ }
17573
+ function findConfigNode(graph, basenames, serviceName) {
17574
+ let scoped = null;
17575
+ let anyMatch = null;
17576
+ graph.forEachNode((id, attrs) => {
17577
+ if (scoped) return;
17578
+ const node = attrs;
17579
+ if (node.type !== import_types82.NodeType.ConfigNode) return;
17580
+ if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
17581
+ if (anyMatch === null) anyMatch = id;
17582
+ if (configNodeService(graph, id) === serviceName) scoped = id;
17583
+ });
17584
+ return scoped ?? anyMatch;
17585
+ }
17586
+ function createEasResolveTarget(graph) {
17587
+ return (signal) => {
17588
+ if (signal.targetKind !== EAS_TARGET_KIND) return null;
17589
+ const identity = parseEasTargetName(signal.targetName);
17590
+ if (!identity) return null;
17591
+ const { serviceName, phase } = identity;
17592
+ const basenames = configBasenamesForPhase(phase);
17593
+ if (basenames.length > 0) {
17594
+ const configNodeId = findConfigNode(graph, basenames, serviceName);
17595
+ if (configNodeId) {
17596
+ return { targetNodeId: configNodeId, serviceName, edgeType: import_types82.EdgeType.CALLS };
17597
+ }
17598
+ }
17599
+ return {
17600
+ targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
17601
+ serviceName,
17602
+ edgeType: import_types82.EdgeType.CALLS
17603
+ };
17604
+ };
17605
+ }
17606
+
17607
+ // src/connectors/eas/index.ts
17608
+ function isBuildSince(build, sinceIso) {
17609
+ const t = Date.parse(buildEventTime(build));
17610
+ const s = Date.parse(sinceIso);
17611
+ if (Number.isNaN(t) || Number.isNaN(s)) return true;
17612
+ return t > s;
17613
+ }
17614
+ function boundedSinceIso2(since, now, maxLookbackMs) {
17615
+ const floor = new Date(now.getTime() - maxLookbackMs);
17616
+ if (!since) return floor.toISOString();
17617
+ const sinceMs = new Date(since).getTime();
17618
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
17619
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
17620
+ }
17621
+ var EasConnector = class {
17622
+ constructor(config, fetchImpl) {
17623
+ this.config = config;
17624
+ this.fetchImpl = fetchImpl;
17625
+ }
17626
+ config;
17627
+ fetchImpl;
17628
+ provider = "eas";
17629
+ async poll(ctx) {
17630
+ const creds = readEasCredentials(ctx.credentials);
17631
+ const serviceName = this.config.serviceName ?? this.config.appId;
17632
+ const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS6;
17633
+ const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
17634
+ const builds = await fetchErroredBuilds(creds.token, this.config, this.fetchImpl);
17635
+ const fresh = builds.filter((b) => isBuildSince(b, sinceIso));
17636
+ const maxLogBytes = this.config.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
17637
+ for (const build of fresh) {
17638
+ build.logsText = await fetchBuildLogs(build.logFileUrls, maxLogBytes, this.fetchImpl);
17639
+ }
17640
+ return mapBuildsToSignals(fresh, serviceName);
17641
+ }
17642
+ };
17643
+ function createEasConnector(graph, config, fetchImpl) {
17644
+ return {
17645
+ connector: new EasConnector(config, fetchImpl),
17646
+ resolveTarget: createEasResolveTarget(graph)
17647
+ };
17648
+ }
17649
+
17180
17650
  // src/connectors/registry.ts
17181
17651
  var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
17182
17652
  async function authProbe(input) {
@@ -17464,6 +17934,41 @@ var PROVIDER_DISPATCH = {
17464
17934
  ...fetchImpl ? { fetchImpl } : {}
17465
17935
  });
17466
17936
  }
17937
+ },
17938
+ eas: {
17939
+ provider: "eas",
17940
+ // The secret is a single robot-user EXPO_TOKEN; a single-string credential
17941
+ // maps to `token`. `appId` is non-secret config (connector-config.md §7.1).
17942
+ primaryCredentialKey: "token",
17943
+ requiredCredentialFields: ["token"],
17944
+ requiredOptionFields: ["appId"],
17945
+ build(graph, options) {
17946
+ return createEasConnector(graph, options);
17947
+ },
17948
+ // Runs the connector's real `builds` query at limit 1 — the exact read poll()
17949
+ // performs, minus the pages — so the probe checks both that the EXPO_TOKEN
17950
+ // authenticates and that this app id is reachable, the same probe-the-real-
17951
+ // query discipline Railway and Cloud Run use over a trivial `{ __typename }`.
17952
+ // A bad or wrong-scoped token comes back as an Expo GraphQL error, which
17953
+ // `fetchErroredBuilds` throws on, so it fails honestly here rather than
17954
+ // silently at the first poll.
17955
+ async validate({ credentials, options, fetchImpl }) {
17956
+ const cfg = options;
17957
+ const appId = String(cfg.appId ?? "");
17958
+ if (!appId) return { ok: false, reason: "eas: appId is required to validate" };
17959
+ const probeConfig = {
17960
+ appId,
17961
+ pageSize: 1,
17962
+ maxPages: 1,
17963
+ ...cfg.apiUrl ? { apiUrl: cfg.apiUrl } : {}
17964
+ };
17965
+ try {
17966
+ await fetchErroredBuilds(String(credentials.token ?? ""), probeConfig, fetchImpl);
17967
+ return { ok: true };
17968
+ } catch (err) {
17969
+ return { ok: false, reason: `eas auth check failed: ${err.message}` };
17970
+ }
17971
+ }
17467
17972
  }
17468
17973
  };
17469
17974
  function vercelCredsFrom(credentials) {
@@ -17645,7 +18150,11 @@ async function startConnectorPolling(input) {
17645
18150
  const stopFns = all.map(
17646
18151
  (registration) => startConnectorPollLoop(
17647
18152
  registration.connector,
17648
- { projectDir: input.projectDir, credentials: registration.credentials },
18153
+ {
18154
+ projectDir: input.projectDir,
18155
+ credentials: registration.credentials,
18156
+ ...input.errorsPath ? { errorsPath: input.errorsPath } : {}
18157
+ },
17649
18158
  input.graph,
17650
18159
  registration.resolveTarget,
17651
18160
  { intervalMs: registration.intervalMs, connectorId: registration.id }
@@ -17815,11 +18324,11 @@ function registerRoutes(scope, ctx) {
17815
18324
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
17816
18325
  const parsed = [];
17817
18326
  for (const c of candidates) {
17818
- const r = import_types80.DivergenceTypeSchema.safeParse(c);
18327
+ const r = import_types85.DivergenceTypeSchema.safeParse(c);
17819
18328
  if (!r.success) {
17820
18329
  return reply.code(400).send({
17821
18330
  error: `unknown divergence type "${c}"`,
17822
- allowed: import_types80.DivergenceTypeSchema.options
18331
+ allowed: import_types85.DivergenceTypeSchema.options
17823
18332
  });
17824
18333
  }
17825
18334
  parsed.push(r.data);
@@ -17926,10 +18435,15 @@ function registerRoutes(scope, ctx) {
17926
18435
  }
17927
18436
  const reg = built.registration;
17928
18437
  const at = (/* @__PURE__ */ new Date()).toISOString();
18438
+ const incidentsPath = errorsPathFor(proj);
17929
18439
  try {
17930
18440
  const result = await ctx.runPoll(
17931
18441
  reg.connector,
17932
- { projectDir: proj.scanPath ?? "", credentials: reg.credentials },
18442
+ {
18443
+ projectDir: proj.scanPath ?? "",
18444
+ credentials: reg.credentials,
18445
+ ...incidentsPath ? { errorsPath: incidentsPath } : {}
18446
+ },
17933
18447
  proj.graph,
17934
18448
  reg.resolveTarget
17935
18449
  );
@@ -18128,7 +18642,7 @@ function registerRoutes(scope, ctx) {
18128
18642
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
18129
18643
  let violations = await log.readAll();
18130
18644
  if (req.query.severity) {
18131
- const sev = import_types80.PolicySeveritySchema.safeParse(req.query.severity);
18645
+ const sev = import_types85.PolicySeveritySchema.safeParse(req.query.severity);
18132
18646
  if (!sev.success) {
18133
18647
  return reply.code(400).send({
18134
18648
  error: "invalid severity",
@@ -18167,7 +18681,7 @@ function registerRoutes(scope, ctx) {
18167
18681
  scope.post("/policies/check", async (req, reply) => {
18168
18682
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
18169
18683
  if (!proj) return;
18170
- const parsed = import_types80.PoliciesCheckBodySchema.safeParse(req.body ?? {});
18684
+ const parsed = import_types85.PoliciesCheckBodySchema.safeParse(req.body ?? {});
18171
18685
  if (!parsed.success) {
18172
18686
  return reply.code(400).send({
18173
18687
  error: "invalid /policies/check body",
@@ -18516,7 +19030,7 @@ function unroutedErrorsPath(neatHome3) {
18516
19030
  }
18517
19031
 
18518
19032
  // src/daemon.ts
18519
- var import_types81 = require("@neat.is/types");
19033
+ var import_types86 = require("@neat.is/types");
18520
19034
  function daemonJsonPath(scanPath) {
18521
19035
  return import_node_path67.default.join(scanPath, "neat-out", "daemon.json");
18522
19036
  }
@@ -18641,7 +19155,7 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
18641
19155
  if (!serviceName) return true;
18642
19156
  if (serviceNameMatchesProject(serviceName, project)) return true;
18643
19157
  return graph.someNode(
18644
- (_id, attrs) => attrs.type === import_types81.NodeType.ServiceNode && attrs.name === serviceName
19158
+ (_id, attrs) => attrs.type === import_types86.NodeType.ServiceNode && attrs.name === serviceName
18645
19159
  );
18646
19160
  }
18647
19161
  async function bootstrapProject(entry, connectors = [], neatHome3) {
@@ -18689,6 +19203,10 @@ async function bootstrapProject(entry, connectors = [], neatHome3) {
18689
19203
  project: entry.name,
18690
19204
  graph,
18691
19205
  projectDir: entry.path,
19206
+ // The slot's incident ledger, so an incident-emitting connector (ADR-185)
19207
+ // writes a build-failure incident onto the same errors.ndjson OTLP-derived
19208
+ // incidents land in.
19209
+ errorsPath: paths.errorsPath,
18692
19210
  ...neatHome3 ? { home: neatHome3 } : {},
18693
19211
  extra: connectors,
18694
19212
  onSkip: (skipped, reason) => console.warn(
@@ -18931,6 +19449,19 @@ async function startDaemon(opts = {}) {
18931
19449
  let otlpAddress = "";
18932
19450
  let daemonRecord = null;
18933
19451
  if (bind) {
19452
+ let bareSpanIsRoutable2 = function(serviceName) {
19453
+ if (singleProject) {
19454
+ const slot = slots.get(singleProject);
19455
+ if (!slot) {
19456
+ return !serviceName || serviceNameMatchesProject(serviceName, singleProject);
19457
+ }
19458
+ return spanBelongsToSingleProject(slot.graph, singleProject, serviceName);
19459
+ }
19460
+ const entries = [...slots.values()].map((s) => s.entry);
19461
+ const target = routeSpanToProject(serviceName, entries);
19462
+ return slots.has(target) || slots.has(DEFAULT_PROJECT);
19463
+ };
19464
+ var bareSpanIsRoutable = bareSpanIsRoutable2;
18934
19465
  const auth = readAuthEnv();
18935
19466
  const host = resolveHost(opts, Boolean(auth.authToken));
18936
19467
  const restPort = resolveRestPort(opts);
@@ -19028,6 +19559,20 @@ async function startDaemon(opts = {}) {
19028
19559
  const liveEntries = await listProjects().catch(() => []);
19029
19560
  let slot = slots.get(project);
19030
19561
  if (!slot) {
19562
+ if (singleProject && project === singleProject) {
19563
+ slot = await tryRecoverSlot({
19564
+ name: singleProject,
19565
+ path: singleProjectPath,
19566
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
19567
+ languages: [],
19568
+ status: "active"
19569
+ });
19570
+ if (!slot || slot.status !== "active") {
19571
+ warnDroppedSpan(singleProject, slot?.errorReason ?? "unknown");
19572
+ return null;
19573
+ }
19574
+ return slot;
19575
+ }
19031
19576
  await recordUnroutedSpan(serviceName, traceId);
19032
19577
  return null;
19033
19578
  }
@@ -19094,7 +19639,30 @@ async function startDaemon(opts = {}) {
19094
19639
  // host, rather than accepting it and dropping the batch. `slots` covers
19095
19640
  // active/recovering projects, `bootstrapStatus` the ones still
19096
19641
  // extracting; a foreign or wrong-cased project name matches neither.
19097
- isProjectRegistered: (project) => slots.has(project) || bootstrapStatus.has(project)
19642
+ // A single-project daemon owns exactly one project by definition, so its
19643
+ // own name always counts as registered even before loadAll populates the
19644
+ // slot — otherwise a scoped span arriving during cold-start would 404 and
19645
+ // be lost (OTLP does not retry 4xx). resolveSlotByName then builds it (#879).
19646
+ isProjectRegistered: (project) => slots.has(project) || bootstrapStatus.has(project) || singleProject !== void 0 && project === singleProject,
19647
+ // #881 — the bare `/v1/traces` route replies before the span is routed
19648
+ // (off the queue), so tell the receiver, per batch, how many spans will
19649
+ // land on no project. It keeps the 200 but reports those as
19650
+ // partialSuccess.rejectedSpans instead of an empty partialSuccess that an
19651
+ // exporter reads as full acceptance. Pure — the drop + unrouted-ledger
19652
+ // write still happen on the async onSpan path.
19653
+ classifyBareRoutability: (spans) => {
19654
+ let rejected = 0;
19655
+ for (const span of spans) {
19656
+ if (!bareSpanIsRoutable2(span.service)) rejected++;
19657
+ }
19658
+ if (rejected === 0) return { rejected: 0 };
19659
+ const noun = rejected === 1 ? "span" : "spans";
19660
+ const verb = rejected === 1 ? "was" : "were";
19661
+ return {
19662
+ rejected,
19663
+ 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.`
19664
+ };
19665
+ }
19098
19666
  });
19099
19667
  otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
19100
19668
  console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);