@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/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) => {
@@ -2213,6 +2229,7 @@ var import_yaml = require("yaml");
2213
2229
  var import_types3 = require("@neat.is/types");
2214
2230
  var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go", ".rb", ".php"]);
2215
2231
  var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
2232
+ var JSON_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["app.json", "app.config.json", "eas.json"]);
2216
2233
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
2217
2234
  "node_modules",
2218
2235
  ".git",
@@ -2252,6 +2269,7 @@ async function isPythonVenvDir(dir) {
2252
2269
  function isConfigFile(name) {
2253
2270
  const ext = import_node_path3.default.extname(name);
2254
2271
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2272
+ if (JSON_CONFIG_FILENAMES.has(name)) return { match: true, fileType: "json" };
2255
2273
  if (name === ".env" || name.startsWith(".env.")) {
2256
2274
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
2257
2275
  if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
@@ -4828,10 +4846,15 @@ function resolveDistToSrc(absFilepath, line) {
4828
4846
  }
4829
4847
  if (!entry2) return null;
4830
4848
  try {
4831
- const pos = entry2.consumer.originalPositionFor({
4832
- line: line !== void 0 && Number.isFinite(line) ? line : 1,
4833
- column: 0
4834
- });
4849
+ const queryLine = line !== void 0 && Number.isFinite(line) ? line : 1;
4850
+ let pos = entry2.consumer.originalPositionFor({ line: queryLine, column: 0 });
4851
+ if (!pos || !pos.source) {
4852
+ pos = entry2.consumer.originalPositionFor({
4853
+ line: queryLine,
4854
+ column: 0,
4855
+ bias: sourceMapJs.SourceMapConsumer.LEAST_UPPER_BOUND
4856
+ });
4857
+ }
4835
4858
  if (!pos || !pos.source) return null;
4836
4859
  const root = entry2.consumer.sourceRoot ?? "";
4837
4860
  const resolved = import_node_path8.default.resolve(entry2.dir, root, pos.source);
@@ -4840,6 +4863,9 @@ function resolveDistToSrc(absFilepath, line) {
4840
4863
  return null;
4841
4864
  }
4842
4865
  }
4866
+ function hasAdjacentSourceMap(absFilepath) {
4867
+ return sourceMapCache.get(absFilepath) != null;
4868
+ }
4843
4869
  function callSiteFromSpan(span, serviceNode, scanPath) {
4844
4870
  const filepath = codeFilepathOf(span.attributes);
4845
4871
  if (filepath === void 0) return null;
@@ -4855,7 +4881,7 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
4855
4881
  }
4856
4882
  const relPath = relPathForRuntimeFile(effectivePath, serviceNode, scanPath);
4857
4883
  if (!relPath) return null;
4858
- if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && serviceNode?.name) {
4884
+ if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && !hasAdjacentSourceMap(abs) && serviceNode?.name) {
4859
4885
  warnNoSourceMaps(serviceNode.name);
4860
4886
  }
4861
4887
  const fn = codeFunctionOf(span.attributes);
@@ -5131,7 +5157,7 @@ function resolveServiceId(graph, host, env) {
5131
5157
  function frontierIdFor(host) {
5132
5158
  return (0, import_types8.frontierId)(host);
5133
5159
  }
5134
- function ensureServiceNode(graph, serviceName, env) {
5160
+ function resolveFusedServiceId(graph, serviceName, env) {
5135
5161
  const id = (0, import_types8.serviceId)(serviceName, env);
5136
5162
  if (graph.hasNode(id)) return id;
5137
5163
  const wanted = serviceName.toLowerCase();
@@ -5141,17 +5167,21 @@ function ensureServiceNode(graph, serviceName, env) {
5141
5167
  if (svc.discoveredVia === "otel") return false;
5142
5168
  return typeof svc.name === "string" && svc.name.toLowerCase() === wanted;
5143
5169
  });
5144
- if (extractedId) return extractedId;
5170
+ return extractedId ?? id;
5171
+ }
5172
+ function ensureServiceNode(graph, serviceName, env) {
5173
+ const resolved = resolveFusedServiceId(graph, serviceName, env);
5174
+ if (graph.hasNode(resolved)) return resolved;
5145
5175
  const node = {
5146
- id,
5176
+ id: resolved,
5147
5177
  type: import_types8.NodeType.ServiceNode,
5148
5178
  name: serviceName,
5149
5179
  language: "unknown",
5150
5180
  discoveredVia: "otel",
5151
5181
  ...env !== "unknown" ? { env } : {}
5152
5182
  };
5153
- graph.addNode(id, node);
5154
- return id;
5183
+ graph.addNode(resolved, node);
5184
+ return resolved;
5155
5185
  }
5156
5186
  function ensureInfraNode(graph, kind, name, provider) {
5157
5187
  const id = (0, import_types8.infraId)(kind, name);
@@ -5344,8 +5374,23 @@ async function appendErrorEvent(ctx, ev) {
5344
5374
  await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(ctx.errorsPath), { recursive: true });
5345
5375
  await import_node_fs7.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
5346
5376
  }
5377
+ async function appendConnectorIncident(errorsPath, input) {
5378
+ const ev = {
5379
+ id: input.id,
5380
+ timestamp: input.timestamp,
5381
+ service: input.service,
5382
+ traceId: input.id,
5383
+ spanId: input.id,
5384
+ errorType: input.errorType,
5385
+ errorMessage: input.errorMessage,
5386
+ ...input.attributes && Object.keys(input.attributes).length > 0 ? { attributes: input.attributes } : {},
5387
+ affectedNode: input.affectedNode
5388
+ };
5389
+ await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(errorsPath), { recursive: true });
5390
+ await import_node_fs7.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
5391
+ }
5347
5392
  function incidentAffectedNode(span, graph, scanPath) {
5348
- const sid = (0, import_types8.serviceId)(span.service, span.env);
5393
+ const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
5349
5394
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
5350
5395
  const callSite = callSiteFromSpan(span, serviceNode, scanPath);
5351
5396
  if (callSite) {
@@ -6744,7 +6789,7 @@ function disambiguate(defs) {
6744
6789
  }
6745
6790
  async function addSymbols(graph, services) {
6746
6791
  const parsers = /* @__PURE__ */ new Map();
6747
- const parserForExt4 = (ext) => {
6792
+ const parserForExt5 = (ext) => {
6748
6793
  const grammar = GRAMMAR_BY_EXT[ext];
6749
6794
  if (!grammar) return null;
6750
6795
  let parser = parsers.get(ext);
@@ -6760,7 +6805,7 @@ async function addSymbols(graph, services) {
6760
6805
  for (const service of services) {
6761
6806
  const files = await loadSourceFiles(service.dir);
6762
6807
  for (const file of files) {
6763
- const parser = parserForExt4(import_node_path17.default.extname(file.path));
6808
+ const parser = parserForExt5(import_node_path17.default.extname(file.path));
6764
6809
  if (!parser) continue;
6765
6810
  const relPath = toPosix(import_node_path17.default.relative(service.dir, file.path));
6766
6811
  let defs;
@@ -6912,7 +6957,7 @@ function stringInner(node) {
6912
6957
  }
6913
6958
  async function addSymbolEdges(graph, services) {
6914
6959
  const parsers = /* @__PURE__ */ new Map();
6915
- const parserForExt4 = (ext) => {
6960
+ const parserForExt5 = (ext) => {
6916
6961
  const grammar = GRAMMAR_BY_EXT[ext];
6917
6962
  if (!grammar) return null;
6918
6963
  let parser = parsers.get(ext);
@@ -6929,7 +6974,7 @@ async function addSymbolEdges(graph, services) {
6929
6974
  const tsPaths = await loadTsPathConfig(service.dir);
6930
6975
  const files = await loadSourceFiles(service.dir);
6931
6976
  for (const file of files) {
6932
- const parser = parserForExt4(import_node_path18.default.extname(file.path));
6977
+ const parser = parserForExt5(import_node_path18.default.extname(file.path));
6933
6978
  if (!parser) continue;
6934
6979
  const relPath = toPosix(import_node_path18.default.relative(service.dir, file.path));
6935
6980
  const fileDir = import_node_path18.default.dirname(file.path);
@@ -7191,7 +7236,7 @@ function firstReferenceLines(root, wanted) {
7191
7236
  }
7192
7237
  async function addServerActions(graph, services) {
7193
7238
  const parsers = /* @__PURE__ */ new Map();
7194
- const parserForExt4 = (ext) => {
7239
+ const parserForExt5 = (ext) => {
7195
7240
  const grammar = GRAMMAR_BY_EXT[ext];
7196
7241
  if (!grammar) return null;
7197
7242
  let parser = parsers.get(ext);
@@ -7214,7 +7259,7 @@ async function addServerActions(graph, services) {
7214
7259
  const files = await loadSourceFiles(service.dir);
7215
7260
  for (const file of files) {
7216
7261
  if (isTestPath(file.path)) continue;
7217
- const parser = parserForExt4(import_node_path19.default.extname(file.path));
7262
+ const parser = parserForExt5(import_node_path19.default.extname(file.path));
7218
7263
  if (!parser) continue;
7219
7264
  const relPath = toPosix(import_node_path19.default.relative(service.dir, file.path));
7220
7265
  let root;
@@ -7277,7 +7322,7 @@ async function addServerActions(graph, services) {
7277
7322
  }
7278
7323
  for (const file of files) {
7279
7324
  if (isTestPath(file.path)) continue;
7280
- const parser = parserForExt4(import_node_path19.default.extname(file.path));
7325
+ const parser = parserForExt5(import_node_path19.default.extname(file.path));
7281
7326
  if (!parser) continue;
7282
7327
  const relPath = toPosix(import_node_path19.default.relative(service.dir, file.path));
7283
7328
  const fileDir = import_node_path19.default.dirname(file.path);
@@ -8241,6 +8286,7 @@ init_cjs_shims();
8241
8286
  var import_node_path30 = __toESM(require("path"), 1);
8242
8287
  var import_tree_sitter6 = __toESM(require("tree-sitter"), 1);
8243
8288
  var import_tree_sitter_javascript4 = __toESM(require("tree-sitter-javascript"), 1);
8289
+ var import_tree_sitter_typescript2 = __toESM(require("tree-sitter-typescript"), 1);
8244
8290
  var import_tree_sitter_python3 = __toESM(require("tree-sitter-python"), 1);
8245
8291
  var import_types20 = require("@neat.is/types");
8246
8292
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
@@ -8291,19 +8337,27 @@ function callsFromSource(source, parser, knownHosts) {
8291
8337
  }
8292
8338
  return out;
8293
8339
  }
8294
- function makeJsParser3() {
8295
- const p = new import_tree_sitter6.default();
8296
- p.setLanguage(import_tree_sitter_javascript4.default);
8297
- return p;
8298
- }
8299
- function makePyParser3() {
8300
- const p = new import_tree_sitter6.default();
8301
- p.setLanguage(import_tree_sitter_python3.default);
8302
- return p;
8340
+ var GRAMMAR_BY_EXT2 = {
8341
+ ".ts": import_tree_sitter_typescript2.default.typescript,
8342
+ ".tsx": import_tree_sitter_typescript2.default.tsx,
8343
+ ".js": import_tree_sitter_javascript4.default,
8344
+ ".jsx": import_tree_sitter_javascript4.default,
8345
+ ".mjs": import_tree_sitter_javascript4.default,
8346
+ ".cjs": import_tree_sitter_javascript4.default,
8347
+ ".py": import_tree_sitter_python3.default
8348
+ };
8349
+ function parserForExt(ext, cache) {
8350
+ const grammar = GRAMMAR_BY_EXT2[ext] ?? import_tree_sitter_javascript4.default;
8351
+ let parser = cache.get(grammar);
8352
+ if (!parser) {
8353
+ parser = new import_tree_sitter6.default();
8354
+ parser.setLanguage(grammar);
8355
+ cache.set(grammar, parser);
8356
+ }
8357
+ return parser;
8303
8358
  }
8304
8359
  async function addHttpCallEdges(graph, services) {
8305
- const jsParser = makeJsParser3();
8306
- const pyParser = makePyParser3();
8360
+ const parserCache = /* @__PURE__ */ new Map();
8307
8361
  const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
8308
8362
  let nodesAdded = 0;
8309
8363
  let edgesAdded = 0;
@@ -8312,7 +8366,7 @@ async function addHttpCallEdges(graph, services) {
8312
8366
  const seen = /* @__PURE__ */ new Set();
8313
8367
  for (const file of files) {
8314
8368
  if (isTestPath(file.path)) continue;
8315
- const parser = import_node_path30.default.extname(file.path) === ".py" ? pyParser : jsParser;
8369
+ const parser = parserForExt(import_node_path30.default.extname(file.path), parserCache);
8316
8370
  let sites;
8317
8371
  try {
8318
8372
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -8385,7 +8439,7 @@ function parseSource5(parser, source) {
8385
8439
  (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK5)
8386
8440
  );
8387
8441
  }
8388
- function makeJsParser4() {
8442
+ function makeJsParser3() {
8389
8443
  const p = new import_tree_sitter7.default();
8390
8444
  p.setLanguage(import_tree_sitter_javascript5.default);
8391
8445
  return p;
@@ -8563,7 +8617,7 @@ function findRoute(entries, method, normalizedPath) {
8563
8617
  );
8564
8618
  }
8565
8619
  async function addRouteCallEdges(graph, services) {
8566
- const jsParser = makeJsParser4();
8620
+ const jsParser = makeJsParser3();
8567
8621
  const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
8568
8622
  const routeIndex = buildRouteIndex(graph);
8569
8623
  if (routeIndex.size === 0) return { nodesAdded: 0, edgesAdded: 0 };
@@ -8955,7 +9009,7 @@ var import_tree_sitter_javascript6 = __toESM(require("tree-sitter-javascript"),
8955
9009
  var import_types27 = require("@neat.is/types");
8956
9010
  var FIRESTORE_CLIENT_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])firebase\/firestore['"`]/;
8957
9011
  var FIRESTORE_ADMIN_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])firebase-admin(?:\/firestore)?['"`]/;
8958
- function parserForExt(ext) {
9012
+ function parserForExt2(ext) {
8959
9013
  const p = new import_tree_sitter8.default();
8960
9014
  p.setLanguage(GRAMMAR_BY_EXT[ext] ?? import_tree_sitter_javascript6.default);
8961
9015
  return p;
@@ -9120,7 +9174,7 @@ function firestoreEndpointsFromFile(file, serviceDir) {
9120
9174
  const hasAdmin = FIRESTORE_ADMIN_IMPORT_RE.test(file.content);
9121
9175
  if (!hasClient && !hasAdmin) return [];
9122
9176
  const fileSdk = hasClient && !hasAdmin ? "client" : hasAdmin && !hasClient ? "admin" : null;
9123
- const tree = parseSource3(parserForExt(import_node_path37.default.extname(file.path)), file.content);
9177
+ const tree = parseSource3(parserForExt2(import_node_path37.default.extname(file.path)), file.content);
9124
9178
  const clientVars = firestoreClientVars(tree.rootNode);
9125
9179
  const collLine = /* @__PURE__ */ new Map();
9126
9180
  const writes = /* @__PURE__ */ new Map();
@@ -9537,7 +9591,7 @@ var import_tree_sitter_python4 = __toESM(require("tree-sitter-python"), 1);
9537
9591
  var import_types29 = require("@neat.is/types");
9538
9592
  var SQLALCHEMY_IMPORT_RE = /(?:from|import)\s+(?:flask_sqlalchemy|sqlalchemy)\b/;
9539
9593
  var PARSE_CHUNK6 = 16384;
9540
- function makePyParser4() {
9594
+ function makePyParser3() {
9541
9595
  const p = new import_tree_sitter9.default();
9542
9596
  p.setLanguage(import_tree_sitter_python4.default);
9543
9597
  return p;
@@ -9644,7 +9698,7 @@ function foreignKeyParentTable(call) {
9644
9698
  }
9645
9699
  function sqlalchemyForeignKeys(file, serviceDir) {
9646
9700
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
9647
- const tree = parseSource6(makePyParser4(), file.content);
9701
+ const tree = parseSource6(makePyParser3(), file.content);
9648
9702
  const out = [];
9649
9703
  const seen = /* @__PURE__ */ new Set();
9650
9704
  walk3(tree.rootNode, (node) => {
@@ -9681,7 +9735,7 @@ function sqlalchemyForeignKeys(file, serviceDir) {
9681
9735
  }
9682
9736
  function sqlalchemyEndpointsFromFile(file, serviceDir) {
9683
9737
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
9684
- const tree = parseSource6(makePyParser4(), file.content);
9738
+ const tree = parseSource6(makePyParser3(), file.content);
9685
9739
  const out = [];
9686
9740
  const seen = /* @__PURE__ */ new Set();
9687
9741
  const push = (name, line, columns) => {
@@ -9733,7 +9787,7 @@ function sqlalchemyEndpointsFromFile(file, serviceDir) {
9733
9787
  function buildSqlalchemyModelRegistry(files) {
9734
9788
  const table = /* @__PURE__ */ new Map();
9735
9789
  const ambiguous = /* @__PURE__ */ new Set();
9736
- const parser = makePyParser4();
9790
+ const parser = makePyParser3();
9737
9791
  for (const file of files) {
9738
9792
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) continue;
9739
9793
  const tree = parseSource6(parser, file.content);
@@ -9782,7 +9836,7 @@ function importsModelName(content, name) {
9782
9836
  function pythonOrmCrossFileEndpoints(files, serviceDir) {
9783
9837
  const registry = buildSqlalchemyModelRegistry(files);
9784
9838
  if (registry.size === 0) return [];
9785
- const parser = makePyParser4();
9839
+ const parser = makePyParser3();
9786
9840
  const out = [];
9787
9841
  const seen = /* @__PURE__ */ new Set();
9788
9842
  for (const file of files) {
@@ -9820,7 +9874,7 @@ var import_tree_sitter_python5 = __toESM(require("tree-sitter-python"), 1);
9820
9874
  var import_types30 = require("@neat.is/types");
9821
9875
  var DJANGO_IMPORT_RE = /(?:from|import)\s+django\b/;
9822
9876
  var PARSE_CHUNK7 = 16384;
9823
- function makePyParser5() {
9877
+ function makePyParser4() {
9824
9878
  const p = new import_tree_sitter10.default();
9825
9879
  p.setLanguage(import_tree_sitter_python5.default);
9826
9880
  return p;
@@ -9881,7 +9935,7 @@ function readMeta(body) {
9881
9935
  }
9882
9936
  function djangoOrmEndpointsFromFile(file, serviceDir) {
9883
9937
  if (!DJANGO_IMPORT_RE.test(file.content)) return [];
9884
- const tree = parseSource7(makePyParser5(), file.content);
9938
+ const tree = parseSource7(makePyParser4(), file.content);
9885
9939
  const out = [];
9886
9940
  const seen = /* @__PURE__ */ new Set();
9887
9941
  const defaultAppLabel = import_node_path40.default.basename(import_node_path40.default.dirname(file.path));
@@ -9916,7 +9970,7 @@ var import_tree_sitter_javascript7 = __toESM(require("tree-sitter-javascript"),
9916
9970
  var import_types31 = require("@neat.is/types");
9917
9971
  var DRIZZLE_IMPORT_RE = /drizzle-orm/;
9918
9972
  var TABLE_BUILDERS = /* @__PURE__ */ new Set(["pgTable", "mysqlTable", "sqliteTable"]);
9919
- function parserForExt2(ext) {
9973
+ function parserForExt3(ext) {
9920
9974
  const p = new import_tree_sitter11.default();
9921
9975
  p.setLanguage(GRAMMAR_BY_EXT[ext] ?? import_tree_sitter_javascript7.default);
9922
9976
  return p;
@@ -9988,7 +10042,7 @@ function columnsFromObject(obj) {
9988
10042
  }
9989
10043
  function drizzleEndpointsFromFile(file, serviceDir) {
9990
10044
  if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
9991
- const tree = parseSource3(parserForExt2(import_node_path41.default.extname(file.path)), file.content);
10045
+ const tree = parseSource3(parserForExt3(import_node_path41.default.extname(file.path)), file.content);
9992
10046
  const out = [];
9993
10047
  const seen = /* @__PURE__ */ new Set();
9994
10048
  const walk9 = (node) => {
@@ -10077,7 +10131,7 @@ function referencesTargetVar(call) {
10077
10131
  }
10078
10132
  function drizzleForeignKeys(file, serviceDir) {
10079
10133
  if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
10080
- const tree = parseSource3(parserForExt2(import_node_path41.default.extname(file.path)), file.content);
10134
+ const tree = parseSource3(parserForExt3(import_node_path41.default.extname(file.path)), file.content);
10081
10135
  const { tables, varToTable } = collectDrizzleTables(tree.rootNode);
10082
10136
  const out = [];
10083
10137
  const seen = /* @__PURE__ */ new Set();
@@ -11145,8 +11199,41 @@ var import_tree_sitter14 = __toESM(require("tree-sitter"), 1);
11145
11199
  var import_tree_sitter_go3 = __toESM(require("tree-sitter-go"), 1);
11146
11200
  var import_types35 = require("@neat.is/types");
11147
11201
  init_otel();
11148
- var SQL_METHODS = /* @__PURE__ */ new Set(["Exec", "ExecContext", "Query", "QueryContext", "QueryRow", "QueryRowContext"]);
11149
11202
  var PARSE_CHUNK10 = 16384;
11203
+ var DATABASE_SQL_METHODS = /* @__PURE__ */ new Set([
11204
+ "Query",
11205
+ "QueryContext",
11206
+ "QueryRow",
11207
+ "QueryRowContext",
11208
+ "Exec",
11209
+ "ExecContext",
11210
+ "Prepare",
11211
+ "PrepareContext"
11212
+ ]);
11213
+ var SQLX_METHODS = /* @__PURE__ */ new Set([
11214
+ "Get",
11215
+ "Select",
11216
+ "Queryx",
11217
+ "QueryRowx",
11218
+ "NamedExec",
11219
+ "NamedQuery",
11220
+ "MustExec",
11221
+ "Preparex",
11222
+ "GetContext",
11223
+ "SelectContext"
11224
+ ]);
11225
+ var DATABASE_SQL_IMPORT = "database/sql";
11226
+ var SQLX_IMPORT = "github.com/jmoiron/sqlx";
11227
+ function makeGoParser3() {
11228
+ const p = new import_tree_sitter14.default();
11229
+ p.setLanguage(import_tree_sitter_go3.default);
11230
+ return p;
11231
+ }
11232
+ function parseSource10(parser, source) {
11233
+ return parser.parse(
11234
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK10)
11235
+ );
11236
+ }
11150
11237
  function walk7(node, visit) {
11151
11238
  visit(node);
11152
11239
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -11154,25 +11241,54 @@ function walk7(node, visit) {
11154
11241
  if (child) walk7(child, visit);
11155
11242
  }
11156
11243
  }
11244
+ function goStringLiteralValue(node) {
11245
+ if (!node) return null;
11246
+ if (node.type === "interpreted_string_literal" || node.type === "raw_string_literal") {
11247
+ const t = node.text;
11248
+ return t.length >= 2 ? t.slice(1, -1) : "";
11249
+ }
11250
+ return null;
11251
+ }
11252
+ function goImportsAny(root, names) {
11253
+ let found = false;
11254
+ walk7(root, (node) => {
11255
+ if (found || node.type !== "import_spec") return;
11256
+ for (let i = 0; i < node.namedChildCount; i++) {
11257
+ const value = goStringLiteralValue(node.namedChild(i));
11258
+ if (value !== null && names.has(value)) found = true;
11259
+ }
11260
+ });
11261
+ return found;
11262
+ }
11263
+ function firstStringLiteralArg(argsNode) {
11264
+ if (!argsNode) return null;
11265
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
11266
+ const value = goStringLiteralValue(argsNode.namedChild(i));
11267
+ if (value !== null) return value;
11268
+ }
11269
+ return null;
11270
+ }
11157
11271
  function goSqlEndpointsFromFile(file, serviceDir) {
11158
11272
  if (import_node_path47.default.extname(file.path) !== ".go") return [];
11159
- const parser = new import_tree_sitter14.default();
11160
- parser.setLanguage(import_tree_sitter_go3.default);
11161
- const tree = parser.parse(
11162
- (index) => index >= file.content.length ? "" : file.content.slice(index, index + PARSE_CHUNK10)
11163
- );
11273
+ if (!file.content.includes(DATABASE_SQL_IMPORT) && !file.content.includes(SQLX_IMPORT)) return [];
11274
+ const tree = parseSource10(makeGoParser3(), file.content);
11275
+ const importsDatabaseSql = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([DATABASE_SQL_IMPORT]));
11276
+ const importsSqlx = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([SQLX_IMPORT]));
11277
+ if (!importsDatabaseSql && !importsSqlx) return [];
11164
11278
  const out = [];
11165
11279
  walk7(tree.rootNode, (node) => {
11166
11280
  if (node.type !== "call_expression") return;
11167
11281
  const fn = node.childForFieldName("function");
11168
11282
  if (fn?.type !== "selector_expression") return;
11169
11283
  const method = fn.childForFieldName("field")?.text;
11170
- if (!method || !SQL_METHODS.has(method)) return;
11171
- const arg = node.childForFieldName("arguments")?.namedChild(0);
11172
- if (!arg || arg.type !== "interpreted_string_literal" && arg.type !== "raw_string_literal") return;
11173
- const sql = arg.text.slice(1, -1);
11284
+ if (!method) return;
11285
+ const recognized = DATABASE_SQL_METHODS.has(method) || importsSqlx && SQLX_METHODS.has(method);
11286
+ if (!recognized) return;
11287
+ const sql = firstStringLiteralArg(node.childForFieldName("arguments"));
11288
+ if (sql === null) return;
11174
11289
  const table = tableFromSqlStatement(sql);
11175
11290
  if (!table) return;
11291
+ const columns = columnsFromSqlStatement(sql);
11176
11292
  const line = node.startPosition.row + 1;
11177
11293
  out.push({
11178
11294
  infraId: (0, import_types35.infraId)("sql-table", table),
@@ -11180,7 +11296,12 @@ function goSqlEndpointsFromFile(file, serviceDir) {
11180
11296
  kind: "sql-table",
11181
11297
  edgeType: "CALLS",
11182
11298
  confidenceKind: "verified-call-site",
11183
- evidence: { file: toPosix(import_node_path47.default.relative(serviceDir, file.path)), line, snippet: snippet(file.content, line) }
11299
+ ...columns.length > 0 ? { columns } : {},
11300
+ evidence: {
11301
+ file: toPosix(import_node_path47.default.relative(serviceDir, file.path)),
11302
+ line,
11303
+ snippet: snippet(file.content, line)
11304
+ }
11184
11305
  });
11185
11306
  });
11186
11307
  return out;
@@ -11194,12 +11315,12 @@ var import_tree_sitter_go4 = __toESM(require("tree-sitter-go"), 1);
11194
11315
  var import_types36 = require("@neat.is/types");
11195
11316
  var GORM_IMPORT_RE = /gorm\.io\/gorm/;
11196
11317
  var PARSE_CHUNK11 = 16384;
11197
- function makeGoParser3() {
11318
+ function makeGoParser4() {
11198
11319
  const p = new import_tree_sitter15.default();
11199
11320
  p.setLanguage(import_tree_sitter_go4.default);
11200
11321
  return p;
11201
11322
  }
11202
- function parseSource10(parser, source) {
11323
+ function parseSource11(parser, source) {
11203
11324
  return parser.parse(
11204
11325
  (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK11)
11205
11326
  );
@@ -11623,7 +11744,7 @@ function collectColumns(struct, structs, seen, prefix, out, emitted) {
11623
11744
  function gormEndpointsFromFile(file, serviceDir) {
11624
11745
  if (import_node_path48.default.extname(file.path) !== ".go") return [];
11625
11746
  if (!GORM_IMPORT_RE.test(file.content)) return [];
11626
- const tree = parseSource10(makeGoParser3(), file.content);
11747
+ const tree = parseSource11(makeGoParser4(), file.content);
11627
11748
  const { structs, models, tableFor } = analyze(tree);
11628
11749
  const out = [];
11629
11750
  const seenTables = /* @__PURE__ */ new Set();
@@ -11654,7 +11775,7 @@ function gormEndpointsFromFile(file, serviceDir) {
11654
11775
  function gormForeignKeys(file, serviceDir) {
11655
11776
  if (import_node_path48.default.extname(file.path) !== ".go") return [];
11656
11777
  if (!GORM_IMPORT_RE.test(file.content)) return [];
11657
- const tree = parseSource10(makeGoParser3(), file.content);
11778
+ const tree = parseSource11(makeGoParser4(), file.content);
11658
11779
  const { structs, models, tableFor } = analyze(tree);
11659
11780
  const out = [];
11660
11781
  const seen = /* @__PURE__ */ new Set();
@@ -12784,7 +12905,7 @@ var import_tree_sitter_javascript8 = __toESM(require("tree-sitter-javascript"),
12784
12905
  var import_types47 = require("@neat.is/types");
12785
12906
  var ZOD_IMPORT_RE = /\bzod\b/;
12786
12907
  var ZOD_OBJECTS = /* @__PURE__ */ new Set(["z", "zod"]);
12787
- function parserForExt3(ext) {
12908
+ function parserForExt4(ext) {
12788
12909
  const p = new import_tree_sitter16.default();
12789
12910
  p.setLanguage(GRAMMAR_BY_EXT[ext] ?? import_tree_sitter_javascript8.default);
12790
12911
  return p;
@@ -12873,7 +12994,7 @@ function topLevelSchemas(root) {
12873
12994
  }
12874
12995
  function zodShapesFromFile(file, serviceDir) {
12875
12996
  if (!ZOD_IMPORT_RE.test(file.content)) return [];
12876
- const tree = parseSource3(parserForExt3(import_node_path57.default.extname(file.path)), file.content);
12997
+ const tree = parseSource3(parserForExt4(import_node_path57.default.extname(file.path)), file.content);
12877
12998
  const out = [];
12878
12999
  const seen = /* @__PURE__ */ new Set();
12879
13000
  for (const { name, call } of topLevelSchemas(tree.rootNode)) {
@@ -13449,7 +13570,7 @@ var Projects = class {
13449
13570
  init_cjs_shims();
13450
13571
  var import_fastify2 = __toESM(require("fastify"), 1);
13451
13572
  var import_cors = __toESM(require("@fastify/cors"), 1);
13452
- var import_types80 = require("@neat.is/types");
13573
+ var import_types85 = require("@neat.is/types");
13453
13574
 
13454
13575
  // src/extend/index.ts
13455
13576
  init_cjs_shims();
@@ -14845,6 +14966,23 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
14845
14966
  unresolved++;
14846
14967
  continue;
14847
14968
  }
14969
+ if (signal.incident) {
14970
+ ensureServiceNode(graph, resolved.serviceName, NO_ENV);
14971
+ if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
14972
+ unresolved++;
14973
+ continue;
14974
+ }
14975
+ await appendConnectorIncident(ctx.errorsPath, {
14976
+ id: signal.incident.id,
14977
+ timestamp: signal.incident.timestamp,
14978
+ service: signal.incident.service,
14979
+ errorType: signal.incident.errorType,
14980
+ errorMessage: signal.incident.errorMessage,
14981
+ ...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
14982
+ affectedNode: resolved.targetNodeId
14983
+ });
14984
+ continue;
14985
+ }
14848
14986
  if (resolved.ensureInfraNode) {
14849
14987
  const { kind, name, provider } = resolved.ensureInfraNode;
14850
14988
  ensureInfraNode(graph, kind, name, provider);
@@ -17079,6 +17217,338 @@ function createPlanetscaleConnector(graph, config, deps = {}) {
17079
17217
  };
17080
17218
  }
17081
17219
 
17220
+ // src/connectors/eas/index.ts
17221
+ init_cjs_shims();
17222
+
17223
+ // src/connectors/eas/client.ts
17224
+ init_cjs_shims();
17225
+
17226
+ // src/connectors/eas/types.ts
17227
+ init_cjs_shims();
17228
+ function readEasCredentials(raw) {
17229
+ const token = raw["token"];
17230
+ if (typeof token !== "string" || token.length === 0) {
17231
+ throw new Error("eas connector: credentials.token (EXPO_TOKEN) must be a non-empty string");
17232
+ }
17233
+ return { token };
17234
+ }
17235
+ var EAS_STATUS_ERRORED = "ERRORED";
17236
+ var TRANSIENT_BUILD_PHASES = /* @__PURE__ */ new Set([
17237
+ "SPIN_UP_BUILDER",
17238
+ "PREPARE_CREDENTIALS",
17239
+ "RESTORE_CACHE",
17240
+ "UPLOAD_APPLICATION_ARCHIVE"
17241
+ ]);
17242
+ var INTERNAL_ERROR_CODE = /INTERNAL_SERVER_ERROR|EAS_BUILD_.*INTERNAL|_INTERNAL_ERROR|UNKNOWN_ERROR/i;
17243
+ function isTransientFailure(err) {
17244
+ if (!err) return false;
17245
+ const phase = typeof err.buildPhase === "string" ? err.buildPhase : void 0;
17246
+ if (phase) {
17247
+ if (TRANSIENT_BUILD_PHASES.has(phase)) return true;
17248
+ if (/^SPIN_UP|_CREDENTIALS$|^RESTORE_CACHE/i.test(phase)) return true;
17249
+ }
17250
+ const code = typeof err.errorCode === "string" ? err.errorCode : void 0;
17251
+ if (code && INTERNAL_ERROR_CODE.test(code)) return true;
17252
+ return false;
17253
+ }
17254
+ var FIELD_SEP3 = "\0";
17255
+ var EAS_TARGET_KIND = "eas-build";
17256
+ function packEasTargetName(identity) {
17257
+ return [identity.serviceName, identity.phase].join(FIELD_SEP3);
17258
+ }
17259
+ function parseEasTargetName(targetName) {
17260
+ const sep = targetName.indexOf(FIELD_SEP3);
17261
+ if (sep === -1) return null;
17262
+ const serviceName = targetName.slice(0, sep);
17263
+ const phase = targetName.slice(sep + 1);
17264
+ if (!serviceName) return null;
17265
+ return { serviceName, phase };
17266
+ }
17267
+
17268
+ // src/connectors/eas/client.ts
17269
+ var DEFAULT_EAS_API_URL = "https://api.expo.dev/graphql";
17270
+ var DEFAULT_PAGE_SIZE = 50;
17271
+ var DEFAULT_MAX_PAGES = 10;
17272
+ var DEFAULT_MAX_LOG_BYTES = 16 * 1024;
17273
+ var DEFAULT_MAX_LOOKBACK_MS6 = 7 * 24 * 60 * 60 * 1e3;
17274
+ var BUILDS_QUERY = `
17275
+ query NeatEasErroredBuilds($appId: String!, $offset: Int!, $limit: Int!) {
17276
+ app {
17277
+ byId(appId: $appId) {
17278
+ builds(offset: $offset, limit: $limit, filter: { status: ERRORED }) {
17279
+ id
17280
+ status
17281
+ platform
17282
+ buildProfile
17283
+ gitCommitHash
17284
+ gitCommitMessage
17285
+ gitRef
17286
+ isGitWorkingTreeDirty
17287
+ createdAt
17288
+ completedAt
17289
+ error {
17290
+ buildPhase
17291
+ errorCode
17292
+ message
17293
+ docsUrl
17294
+ }
17295
+ logFileUrls
17296
+ }
17297
+ }
17298
+ }
17299
+ }
17300
+ `;
17301
+ async function easGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
17302
+ const res = await junctionFetch(
17303
+ apiUrl,
17304
+ {
17305
+ method: "POST",
17306
+ headers: {
17307
+ "Content-Type": "application/json",
17308
+ ...bearerAuthHeader(token)
17309
+ },
17310
+ body: JSON.stringify({ query, variables })
17311
+ },
17312
+ // accountKey: the Expo app id — the per-(provider, accountKey) rate-limit
17313
+ // bucket (ADR-131), the closest thing this connector carries to "one account".
17314
+ { provider: "eas", accountKey, ...fetchImpl ? { fetchImpl } : {} }
17315
+ );
17316
+ if (!res.ok) {
17317
+ throw new Error(`Expo GraphQL request failed: ${res.status} ${res.statusText}`);
17318
+ }
17319
+ const body = await res.json();
17320
+ if (body.errors && body.errors.length > 0) {
17321
+ throw new Error(`Expo GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
17322
+ }
17323
+ if (!body.data) throw new Error("Expo GraphQL response carried no data");
17324
+ return body.data;
17325
+ }
17326
+ async function fetchErroredBuilds(token, config, fetchImpl) {
17327
+ const apiUrl = config.apiUrl ?? DEFAULT_EAS_API_URL;
17328
+ const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
17329
+ const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
17330
+ const out = [];
17331
+ const seen = /* @__PURE__ */ new Set();
17332
+ for (let page = 0; page < maxPages; page++) {
17333
+ const data = await easGraphQL(
17334
+ apiUrl,
17335
+ token,
17336
+ BUILDS_QUERY,
17337
+ { appId: config.appId, offset: page * pageSize, limit: pageSize },
17338
+ config.appId,
17339
+ fetchImpl
17340
+ );
17341
+ const builds = data.app?.byId?.builds;
17342
+ if (!Array.isArray(builds)) break;
17343
+ let added = 0;
17344
+ for (const b of builds) {
17345
+ if (!b || typeof b.id !== "string" || b.id.length === 0) continue;
17346
+ if (b.status !== EAS_STATUS_ERRORED) continue;
17347
+ if (seen.has(b.id)) continue;
17348
+ seen.add(b.id);
17349
+ out.push(b);
17350
+ added++;
17351
+ }
17352
+ if (builds.length < pageSize) break;
17353
+ if (added === 0) break;
17354
+ }
17355
+ return out;
17356
+ }
17357
+ async function fetchBuildLogs(logFileUrls, maxBytes = DEFAULT_MAX_LOG_BYTES, fetchImpl) {
17358
+ if (!Array.isArray(logFileUrls) || logFileUrls.length === 0) return "";
17359
+ const doFetch = fetchImpl ?? fetch;
17360
+ const chunks = [];
17361
+ for (const url of logFileUrls) {
17362
+ if (typeof url !== "string" || url.length === 0) continue;
17363
+ try {
17364
+ const res = await doFetch(url);
17365
+ if (!res.ok) continue;
17366
+ chunks.push(await res.text());
17367
+ } catch {
17368
+ }
17369
+ }
17370
+ const joined = chunks.join("\n");
17371
+ return joined.length > maxBytes ? joined.slice(joined.length - maxBytes) : joined;
17372
+ }
17373
+
17374
+ // src/connectors/eas/map.ts
17375
+ init_cjs_shims();
17376
+ function buildEventTime(build) {
17377
+ if (typeof build.completedAt === "string" && build.completedAt.length > 0) return build.completedAt;
17378
+ if (typeof build.createdAt === "string" && build.createdAt.length > 0) return build.createdAt;
17379
+ return (/* @__PURE__ */ new Date()).toISOString();
17380
+ }
17381
+ function incidentMessage2(build) {
17382
+ const err = build.error ?? {};
17383
+ const phase = typeof err.buildPhase === "string" && err.buildPhase.length > 0 ? ` at ${err.buildPhase}` : "";
17384
+ 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";
17385
+ let msg = `EAS build failed${phase}: ${detail}`;
17386
+ if (build.isGitWorkingTreeDirty === true) {
17387
+ msg += " (built from a dirty working tree \u2014 the commit may not represent what built)";
17388
+ }
17389
+ return msg;
17390
+ }
17391
+ function incidentAttributes(build) {
17392
+ const attrs = {};
17393
+ const err = build.error ?? {};
17394
+ const put = (k, v) => {
17395
+ if (typeof v === "string" && v.length === 0) return;
17396
+ if (v !== void 0 && v !== null) attrs[k] = v;
17397
+ };
17398
+ put("eas.buildId", build.id);
17399
+ put("eas.platform", build.platform ?? void 0);
17400
+ put("eas.buildProfile", build.buildProfile ?? void 0);
17401
+ put("eas.buildPhase", err.buildPhase ?? void 0);
17402
+ put("eas.errorCode", err.errorCode ?? void 0);
17403
+ put("eas.docsUrl", err.docsUrl ?? void 0);
17404
+ put("eas.gitCommitHash", build.gitCommitHash ?? void 0);
17405
+ put("eas.gitRef", build.gitRef ?? void 0);
17406
+ put("eas.gitCommitMessage", build.gitCommitMessage ?? void 0);
17407
+ if (typeof build.isGitWorkingTreeDirty === "boolean") {
17408
+ attrs["eas.gitWorkingTreeDirty"] = build.isGitWorkingTreeDirty;
17409
+ if (build.isGitWorkingTreeDirty) attrs["eas.confidence"] = "low";
17410
+ }
17411
+ put("eas.createdAt", build.createdAt ?? void 0);
17412
+ put("eas.completedAt", build.completedAt ?? void 0);
17413
+ if (typeof build.logsText === "string" && build.logsText.length > 0) {
17414
+ attrs["eas.logs"] = build.logsText;
17415
+ }
17416
+ return attrs;
17417
+ }
17418
+ function mapBuildToSignal(build, serviceName) {
17419
+ if (!build || typeof build !== "object") return null;
17420
+ if (build.status !== EAS_STATUS_ERRORED) return null;
17421
+ if (!build.error) return null;
17422
+ if (isTransientFailure(build.error)) return null;
17423
+ const timestamp = buildEventTime(build);
17424
+ const phase = typeof build.error.buildPhase === "string" ? build.error.buildPhase : "";
17425
+ return {
17426
+ targetKind: EAS_TARGET_KIND,
17427
+ targetName: packEasTargetName({ serviceName, phase }),
17428
+ // Incident-only — no edge, so no call/error count to replay.
17429
+ callCount: 0,
17430
+ errorCount: 0,
17431
+ lastObservedIso: timestamp,
17432
+ incident: {
17433
+ id: `eas:build:${build.id}`,
17434
+ timestamp,
17435
+ service: serviceName,
17436
+ errorType: "eas-build-failure",
17437
+ errorMessage: incidentMessage2(build),
17438
+ attributes: incidentAttributes(build)
17439
+ }
17440
+ };
17441
+ }
17442
+ function mapBuildsToSignals(builds, serviceName) {
17443
+ const out = [];
17444
+ for (const build of builds) {
17445
+ const signal = mapBuildToSignal(build, serviceName);
17446
+ if (signal) out.push(signal);
17447
+ }
17448
+ return out;
17449
+ }
17450
+
17451
+ // src/connectors/eas/resolve.ts
17452
+ init_cjs_shims();
17453
+ var import_types82 = require("@neat.is/types");
17454
+ var NO_ENV2 = "unknown";
17455
+ var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
17456
+ var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
17457
+ "READ_APP_CONFIG",
17458
+ "CONFIGURE_EXPO_UPDATES",
17459
+ "CALCULATE_EXPO_UPDATES_RUNTIME_VERSION"
17460
+ ]);
17461
+ function configBasenamesForPhase(phase) {
17462
+ if (EAS_JSON_PHASES.has(phase)) return ["eas.json"];
17463
+ if (APP_CONFIG_PHASES.has(phase)) return ["app.json", "app.config.json"];
17464
+ return [];
17465
+ }
17466
+ function configNodeService(graph, configNodeId) {
17467
+ for (const edgeId of graph.inboundEdges(configNodeId)) {
17468
+ const edge = graph.getEdgeAttributes(edgeId);
17469
+ if (edge.type !== import_types82.EdgeType.CONFIGURED_BY) continue;
17470
+ const parsed = (0, import_types82.parseFileId)(edge.source);
17471
+ if (parsed) return parsed.service;
17472
+ }
17473
+ return null;
17474
+ }
17475
+ function findConfigNode(graph, basenames, serviceName) {
17476
+ let scoped = null;
17477
+ let anyMatch = null;
17478
+ graph.forEachNode((id, attrs) => {
17479
+ if (scoped) return;
17480
+ const node = attrs;
17481
+ if (node.type !== import_types82.NodeType.ConfigNode) return;
17482
+ if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
17483
+ if (anyMatch === null) anyMatch = id;
17484
+ if (configNodeService(graph, id) === serviceName) scoped = id;
17485
+ });
17486
+ return scoped ?? anyMatch;
17487
+ }
17488
+ function createEasResolveTarget(graph) {
17489
+ return (signal) => {
17490
+ if (signal.targetKind !== EAS_TARGET_KIND) return null;
17491
+ const identity = parseEasTargetName(signal.targetName);
17492
+ if (!identity) return null;
17493
+ const { serviceName, phase } = identity;
17494
+ const basenames = configBasenamesForPhase(phase);
17495
+ if (basenames.length > 0) {
17496
+ const configNodeId = findConfigNode(graph, basenames, serviceName);
17497
+ if (configNodeId) {
17498
+ return { targetNodeId: configNodeId, serviceName, edgeType: import_types82.EdgeType.CALLS };
17499
+ }
17500
+ }
17501
+ return {
17502
+ targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
17503
+ serviceName,
17504
+ edgeType: import_types82.EdgeType.CALLS
17505
+ };
17506
+ };
17507
+ }
17508
+
17509
+ // src/connectors/eas/index.ts
17510
+ function isBuildSince(build, sinceIso) {
17511
+ const t = Date.parse(buildEventTime(build));
17512
+ const s = Date.parse(sinceIso);
17513
+ if (Number.isNaN(t) || Number.isNaN(s)) return true;
17514
+ return t > s;
17515
+ }
17516
+ function boundedSinceIso2(since, now, maxLookbackMs) {
17517
+ const floor = new Date(now.getTime() - maxLookbackMs);
17518
+ if (!since) return floor.toISOString();
17519
+ const sinceMs = new Date(since).getTime();
17520
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
17521
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
17522
+ }
17523
+ var EasConnector = class {
17524
+ constructor(config, fetchImpl) {
17525
+ this.config = config;
17526
+ this.fetchImpl = fetchImpl;
17527
+ }
17528
+ config;
17529
+ fetchImpl;
17530
+ provider = "eas";
17531
+ async poll(ctx) {
17532
+ const creds = readEasCredentials(ctx.credentials);
17533
+ const serviceName = this.config.serviceName ?? this.config.appId;
17534
+ const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS6;
17535
+ const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
17536
+ const builds = await fetchErroredBuilds(creds.token, this.config, this.fetchImpl);
17537
+ const fresh = builds.filter((b) => isBuildSince(b, sinceIso));
17538
+ const maxLogBytes = this.config.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
17539
+ for (const build of fresh) {
17540
+ build.logsText = await fetchBuildLogs(build.logFileUrls, maxLogBytes, this.fetchImpl);
17541
+ }
17542
+ return mapBuildsToSignals(fresh, serviceName);
17543
+ }
17544
+ };
17545
+ function createEasConnector(graph, config, fetchImpl) {
17546
+ return {
17547
+ connector: new EasConnector(config, fetchImpl),
17548
+ resolveTarget: createEasResolveTarget(graph)
17549
+ };
17550
+ }
17551
+
17082
17552
  // src/connectors/registry.ts
17083
17553
  var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
17084
17554
  async function authProbe(input) {
@@ -17366,6 +17836,41 @@ var PROVIDER_DISPATCH = {
17366
17836
  ...fetchImpl ? { fetchImpl } : {}
17367
17837
  });
17368
17838
  }
17839
+ },
17840
+ eas: {
17841
+ provider: "eas",
17842
+ // The secret is a single robot-user EXPO_TOKEN; a single-string credential
17843
+ // maps to `token`. `appId` is non-secret config (connector-config.md §7.1).
17844
+ primaryCredentialKey: "token",
17845
+ requiredCredentialFields: ["token"],
17846
+ requiredOptionFields: ["appId"],
17847
+ build(graph, options) {
17848
+ return createEasConnector(graph, options);
17849
+ },
17850
+ // Runs the connector's real `builds` query at limit 1 — the exact read poll()
17851
+ // performs, minus the pages — so the probe checks both that the EXPO_TOKEN
17852
+ // authenticates and that this app id is reachable, the same probe-the-real-
17853
+ // query discipline Railway and Cloud Run use over a trivial `{ __typename }`.
17854
+ // A bad or wrong-scoped token comes back as an Expo GraphQL error, which
17855
+ // `fetchErroredBuilds` throws on, so it fails honestly here rather than
17856
+ // silently at the first poll.
17857
+ async validate({ credentials, options, fetchImpl }) {
17858
+ const cfg = options;
17859
+ const appId = String(cfg.appId ?? "");
17860
+ if (!appId) return { ok: false, reason: "eas: appId is required to validate" };
17861
+ const probeConfig = {
17862
+ appId,
17863
+ pageSize: 1,
17864
+ maxPages: 1,
17865
+ ...cfg.apiUrl ? { apiUrl: cfg.apiUrl } : {}
17866
+ };
17867
+ try {
17868
+ await fetchErroredBuilds(String(credentials.token ?? ""), probeConfig, fetchImpl);
17869
+ return { ok: true };
17870
+ } catch (err) {
17871
+ return { ok: false, reason: `eas auth check failed: ${err.message}` };
17872
+ }
17873
+ }
17369
17874
  }
17370
17875
  };
17371
17876
  function vercelCredsFrom(credentials) {
@@ -17547,7 +18052,11 @@ async function startConnectorPolling(input) {
17547
18052
  const stopFns = all.map(
17548
18053
  (registration) => startConnectorPollLoop(
17549
18054
  registration.connector,
17550
- { projectDir: input.projectDir, credentials: registration.credentials },
18055
+ {
18056
+ projectDir: input.projectDir,
18057
+ credentials: registration.credentials,
18058
+ ...input.errorsPath ? { errorsPath: input.errorsPath } : {}
18059
+ },
17551
18060
  input.graph,
17552
18061
  registration.resolveTarget,
17553
18062
  { intervalMs: registration.intervalMs, connectorId: registration.id }
@@ -17717,11 +18226,11 @@ function registerRoutes(scope, ctx) {
17717
18226
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
17718
18227
  const parsed = [];
17719
18228
  for (const c of candidates) {
17720
- const r = import_types80.DivergenceTypeSchema.safeParse(c);
18229
+ const r = import_types85.DivergenceTypeSchema.safeParse(c);
17721
18230
  if (!r.success) {
17722
18231
  return reply.code(400).send({
17723
18232
  error: `unknown divergence type "${c}"`,
17724
- allowed: import_types80.DivergenceTypeSchema.options
18233
+ allowed: import_types85.DivergenceTypeSchema.options
17725
18234
  });
17726
18235
  }
17727
18236
  parsed.push(r.data);
@@ -17828,10 +18337,15 @@ function registerRoutes(scope, ctx) {
17828
18337
  }
17829
18338
  const reg = built.registration;
17830
18339
  const at = (/* @__PURE__ */ new Date()).toISOString();
18340
+ const incidentsPath = errorsPathFor(proj);
17831
18341
  try {
17832
18342
  const result = await ctx.runPoll(
17833
18343
  reg.connector,
17834
- { projectDir: proj.scanPath ?? "", credentials: reg.credentials },
18344
+ {
18345
+ projectDir: proj.scanPath ?? "",
18346
+ credentials: reg.credentials,
18347
+ ...incidentsPath ? { errorsPath: incidentsPath } : {}
18348
+ },
17835
18349
  proj.graph,
17836
18350
  reg.resolveTarget
17837
18351
  );
@@ -18030,7 +18544,7 @@ function registerRoutes(scope, ctx) {
18030
18544
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
18031
18545
  let violations = await log.readAll();
18032
18546
  if (req2.query.severity) {
18033
- const sev = import_types80.PolicySeveritySchema.safeParse(req2.query.severity);
18547
+ const sev = import_types85.PolicySeveritySchema.safeParse(req2.query.severity);
18034
18548
  if (!sev.success) {
18035
18549
  return reply.code(400).send({
18036
18550
  error: "invalid severity",
@@ -18069,7 +18583,7 @@ function registerRoutes(scope, ctx) {
18069
18583
  scope.post("/policies/check", async (req2, reply) => {
18070
18584
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
18071
18585
  if (!proj) return;
18072
- const parsed = import_types80.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
18586
+ const parsed = import_types85.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
18073
18587
  if (!parsed.success) {
18074
18588
  return reply.code(400).send({
18075
18589
  error: "invalid /policies/check body",
@@ -18410,7 +18924,7 @@ function unroutedErrorsPath(neatHome4) {
18410
18924
  }
18411
18925
 
18412
18926
  // src/daemon.ts
18413
- var import_types81 = require("@neat.is/types");
18927
+ var import_types86 = require("@neat.is/types");
18414
18928
  function daemonJsonPath(scanPath) {
18415
18929
  return import_node_path67.default.join(scanPath, "neat-out", "daemon.json");
18416
18930
  }
@@ -18549,7 +19063,7 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
18549
19063
  if (!serviceName) return true;
18550
19064
  if (serviceNameMatchesProject(serviceName, project)) return true;
18551
19065
  return graph.someNode(
18552
- (_id, attrs) => attrs.type === import_types81.NodeType.ServiceNode && attrs.name === serviceName
19066
+ (_id, attrs) => attrs.type === import_types86.NodeType.ServiceNode && attrs.name === serviceName
18553
19067
  );
18554
19068
  }
18555
19069
  async function bootstrapProject(entry2, connectors = [], neatHome4) {
@@ -18597,6 +19111,10 @@ async function bootstrapProject(entry2, connectors = [], neatHome4) {
18597
19111
  project: entry2.name,
18598
19112
  graph,
18599
19113
  projectDir: entry2.path,
19114
+ // The slot's incident ledger, so an incident-emitting connector (ADR-185)
19115
+ // writes a build-failure incident onto the same errors.ndjson OTLP-derived
19116
+ // incidents land in.
19117
+ errorsPath: paths.errorsPath,
18600
19118
  ...neatHome4 ? { home: neatHome4 } : {},
18601
19119
  extra: connectors,
18602
19120
  onSkip: (skipped, reason) => console.warn(
@@ -18839,6 +19357,19 @@ async function startDaemon(opts = {}) {
18839
19357
  let otlpAddress = "";
18840
19358
  let daemonRecord = null;
18841
19359
  if (bind) {
19360
+ let bareSpanIsRoutable2 = function(serviceName) {
19361
+ if (singleProject) {
19362
+ const slot = slots.get(singleProject);
19363
+ if (!slot) {
19364
+ return !serviceName || serviceNameMatchesProject(serviceName, singleProject);
19365
+ }
19366
+ return spanBelongsToSingleProject(slot.graph, singleProject, serviceName);
19367
+ }
19368
+ const entries = [...slots.values()].map((s) => s.entry);
19369
+ const target = routeSpanToProject(serviceName, entries);
19370
+ return slots.has(target) || slots.has(DEFAULT_PROJECT);
19371
+ };
19372
+ var bareSpanIsRoutable = bareSpanIsRoutable2;
18842
19373
  const auth = readAuthEnv();
18843
19374
  const host = resolveHost(opts, Boolean(auth.authToken));
18844
19375
  const restPort = resolveRestPort(opts);
@@ -18936,6 +19467,20 @@ async function startDaemon(opts = {}) {
18936
19467
  const liveEntries = await listProjects().catch(() => []);
18937
19468
  let slot = slots.get(project);
18938
19469
  if (!slot) {
19470
+ if (singleProject && project === singleProject) {
19471
+ slot = await tryRecoverSlot({
19472
+ name: singleProject,
19473
+ path: singleProjectPath,
19474
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
19475
+ languages: [],
19476
+ status: "active"
19477
+ });
19478
+ if (!slot || slot.status !== "active") {
19479
+ warnDroppedSpan(singleProject, slot?.errorReason ?? "unknown");
19480
+ return null;
19481
+ }
19482
+ return slot;
19483
+ }
18939
19484
  await recordUnroutedSpan(serviceName, traceId);
18940
19485
  return null;
18941
19486
  }
@@ -19002,7 +19547,30 @@ async function startDaemon(opts = {}) {
19002
19547
  // host, rather than accepting it and dropping the batch. `slots` covers
19003
19548
  // active/recovering projects, `bootstrapStatus` the ones still
19004
19549
  // extracting; a foreign or wrong-cased project name matches neither.
19005
- isProjectRegistered: (project) => slots.has(project) || bootstrapStatus.has(project)
19550
+ // A single-project daemon owns exactly one project by definition, so its
19551
+ // own name always counts as registered even before loadAll populates the
19552
+ // slot — otherwise a scoped span arriving during cold-start would 404 and
19553
+ // be lost (OTLP does not retry 4xx). resolveSlotByName then builds it (#879).
19554
+ isProjectRegistered: (project) => slots.has(project) || bootstrapStatus.has(project) || singleProject !== void 0 && project === singleProject,
19555
+ // #881 — the bare `/v1/traces` route replies before the span is routed
19556
+ // (off the queue), so tell the receiver, per batch, how many spans will
19557
+ // land on no project. It keeps the 200 but reports those as
19558
+ // partialSuccess.rejectedSpans instead of an empty partialSuccess that an
19559
+ // exporter reads as full acceptance. Pure — the drop + unrouted-ledger
19560
+ // write still happen on the async onSpan path.
19561
+ classifyBareRoutability: (spans) => {
19562
+ let rejected = 0;
19563
+ for (const span of spans) {
19564
+ if (!bareSpanIsRoutable2(span.service)) rejected++;
19565
+ }
19566
+ if (rejected === 0) return { rejected: 0 };
19567
+ const noun = rejected === 1 ? "span" : "spans";
19568
+ const verb = rejected === 1 ? "was" : "were";
19569
+ return {
19570
+ rejected,
19571
+ 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.`
19572
+ };
19573
+ }
19006
19574
  });
19007
19575
  otlpAddress = await listenSteppingOtlp(otlpApp, otlpPort, host);
19008
19576
  console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);