@neat.is/core 0.7.4 → 0.7.5

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.
@@ -7662,6 +7662,55 @@ function columnsFromClassBody(body) {
7662
7662
  }
7663
7663
  return out;
7664
7664
  }
7665
+ function foreignKeyParentTable(call) {
7666
+ const fn = call.childForFieldName("function");
7667
+ const t = fn?.text;
7668
+ if (!t) return null;
7669
+ const base = t.includes(".") ? t.slice(t.lastIndexOf(".") + 1) : t;
7670
+ if (base !== "ForeignKey") return null;
7671
+ const target = firstPositionalString(call);
7672
+ if (!target) return null;
7673
+ const parts = target.split(".");
7674
+ if (parts.length < 2) return null;
7675
+ return parts[parts.length - 2];
7676
+ }
7677
+ function sqlalchemyForeignKeys(file, serviceDir) {
7678
+ if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
7679
+ const tree = parseSource6(makePyParser4(), file.content);
7680
+ const out = [];
7681
+ const seen = /* @__PURE__ */ new Set();
7682
+ walk3(tree.rootNode, (node) => {
7683
+ if (node.type !== "class_definition") return;
7684
+ const body = node.childForFieldName("body");
7685
+ const nameNode = node.childForFieldName("name");
7686
+ if (!body || !nameNode) return;
7687
+ const explicit = explicitTablename(body);
7688
+ if (explicit === "computed") return;
7689
+ let childTable = null;
7690
+ if (explicit) childTable = explicit.name;
7691
+ else if (extendsFlaskModel(node)) childTable = flaskSqlalchemyTableName(nameNode.text);
7692
+ if (!childTable) return;
7693
+ walk3(body, (n) => {
7694
+ if (n.type !== "call") return;
7695
+ const parentTable = foreignKeyParentTable(n);
7696
+ if (!parentTable) return;
7697
+ const key = `${childTable}->${parentTable}`;
7698
+ if (seen.has(key)) return;
7699
+ seen.add(key);
7700
+ const line = n.startPosition.row + 1;
7701
+ out.push({
7702
+ childTable,
7703
+ parentTable,
7704
+ evidence: {
7705
+ file: path35.relative(serviceDir, file.path),
7706
+ line,
7707
+ snippet: snippet(file.content, line)
7708
+ }
7709
+ });
7710
+ });
7711
+ });
7712
+ return out;
7713
+ }
7665
7714
  function sqlalchemyEndpointsFromFile(file, serviceDir) {
7666
7715
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
7667
7716
  const tree = parseSource6(makePyParser4(), file.content);
@@ -8005,6 +8054,92 @@ function drizzleEndpointsFromFile(file, serviceDir) {
8005
8054
  walk6(tree.rootNode);
8006
8055
  return out;
8007
8056
  }
8057
+ function enclosingVarName(call) {
8058
+ let node = call;
8059
+ while (node?.parent) {
8060
+ const parent = node.parent;
8061
+ if (parent.type === "variable_declarator") {
8062
+ const name = parent.childForFieldName("name");
8063
+ return name?.type === "identifier" ? name.text : null;
8064
+ }
8065
+ if (parent.type === "call_expression" || parent.type === "member_expression") {
8066
+ node = parent;
8067
+ continue;
8068
+ }
8069
+ return null;
8070
+ }
8071
+ return null;
8072
+ }
8073
+ function collectDrizzleTables(root) {
8074
+ const tables = [];
8075
+ const varToTable = /* @__PURE__ */ new Map();
8076
+ const walk6 = (node) => {
8077
+ if (node.type === "call_expression") {
8078
+ const fn = node.childForFieldName("function");
8079
+ if (fn?.type === "identifier" && TABLE_BUILDERS.has(fn.text)) {
8080
+ const args = node.childForFieldName("arguments");
8081
+ const argNodes = args ? namedChildren3(args) : [];
8082
+ const tableName = stringLiteralText2(argNodes[0] ?? null);
8083
+ const obj = argNodes[1]?.type === "object" ? argNodes[1] : null;
8084
+ if (tableName) {
8085
+ tables.push({ tableName, object: obj });
8086
+ const varName = enclosingVarName(node);
8087
+ if (varName) varToTable.set(varName, tableName);
8088
+ }
8089
+ }
8090
+ }
8091
+ for (const c of namedChildren3(node)) walk6(c);
8092
+ };
8093
+ walk6(root);
8094
+ return { tables, varToTable };
8095
+ }
8096
+ function referencesTargetVar(call) {
8097
+ const fn = call.childForFieldName("function");
8098
+ if (fn?.type !== "member_expression") return null;
8099
+ if (fn.childForFieldName("property")?.text !== "references") return null;
8100
+ const args = call.childForFieldName("arguments");
8101
+ const first = args ? namedChildren3(args)[0] ?? null : null;
8102
+ if (first?.type !== "arrow_function") return null;
8103
+ const body = first.childForFieldName("body");
8104
+ if (body?.type !== "member_expression") return null;
8105
+ const obj = body.childForFieldName("object");
8106
+ return obj?.type === "identifier" ? obj.text : null;
8107
+ }
8108
+ function drizzleForeignKeys(file, serviceDir) {
8109
+ if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
8110
+ const tree = parseSource3(parserForExt(path37.extname(file.path)), file.content);
8111
+ const { tables, varToTable } = collectDrizzleTables(tree.rootNode);
8112
+ const out = [];
8113
+ const seen = /* @__PURE__ */ new Set();
8114
+ for (const table of tables) {
8115
+ if (!table.object) continue;
8116
+ const walk6 = (node) => {
8117
+ if (node.type === "call_expression") {
8118
+ const targetVar = referencesTargetVar(node);
8119
+ const parentTable = targetVar ? varToTable.get(targetVar) : void 0;
8120
+ if (parentTable) {
8121
+ const key = `${table.tableName}->${parentTable}`;
8122
+ if (!seen.has(key)) {
8123
+ seen.add(key);
8124
+ const line = node.startPosition.row + 1;
8125
+ out.push({
8126
+ childTable: table.tableName,
8127
+ parentTable,
8128
+ evidence: {
8129
+ file: path37.relative(serviceDir, file.path),
8130
+ line,
8131
+ snippet: snippet(file.content, line)
8132
+ }
8133
+ });
8134
+ }
8135
+ }
8136
+ }
8137
+ for (const c of namedChildren3(node)) walk6(c);
8138
+ };
8139
+ walk6(table.object);
8140
+ }
8141
+ return out;
8142
+ }
8008
8143
 
8009
8144
  // src/extract/calls/prisma.ts
8010
8145
  import path38 from "path";
@@ -8132,6 +8267,96 @@ async function prismaColumnEndpoints(serviceDir) {
8132
8267
  if (!content) return [];
8133
8268
  return prismaColumnsFromSchema({ path: schemaPath, content }, serviceDir);
8134
8269
  }
8270
+ function buildModelTableMap(lines) {
8271
+ const map = /* @__PURE__ */ new Map();
8272
+ let current = null;
8273
+ let depth = 0;
8274
+ for (const raw of lines) {
8275
+ if (current === null) {
8276
+ const header = raw.match(/^\s*model\s+([A-Za-z_]\w*)\b/);
8277
+ if (header && raw.includes("{")) {
8278
+ current = { model: header[1], table: header[1] };
8279
+ depth = netBraces(raw);
8280
+ if (depth <= 0) {
8281
+ map.set(current.model, current.table);
8282
+ current = null;
8283
+ }
8284
+ }
8285
+ continue;
8286
+ }
8287
+ depth += netBraces(raw);
8288
+ const trimmed = stripLineComment(raw).trim();
8289
+ if (trimmed.startsWith("@@")) {
8290
+ const m = trimmed.match(/@@map\(\s*"([^"]+)"\s*\)/);
8291
+ if (m) current.table = m[1];
8292
+ }
8293
+ if (depth <= 0) {
8294
+ map.set(current.model, current.table);
8295
+ current = null;
8296
+ }
8297
+ }
8298
+ if (current) map.set(current.model, current.table);
8299
+ return map;
8300
+ }
8301
+ function prismaForeignKeysFromSchema(file, serviceDir) {
8302
+ const content = file.content;
8303
+ if (!/\bmodel\s+[A-Za-z_]\w*\s*\{/.test(content)) return [];
8304
+ const lines = content.split("\n");
8305
+ const modelToTable = buildModelTableMap(lines);
8306
+ const out = [];
8307
+ const seen = /* @__PURE__ */ new Set();
8308
+ let current = null;
8309
+ let depth = 0;
8310
+ for (let i = 0; i < lines.length; i++) {
8311
+ const raw = lines[i];
8312
+ const lineNo = i + 1;
8313
+ if (current === null) {
8314
+ const header = raw.match(/^\s*model\s+([A-Za-z_]\w*)\b/);
8315
+ if (header && raw.includes("{")) {
8316
+ current = { table: modelToTable.get(header[1]) ?? header[1] };
8317
+ depth = netBraces(raw);
8318
+ if (depth <= 0) current = null;
8319
+ }
8320
+ continue;
8321
+ }
8322
+ depth += netBraces(raw);
8323
+ const closing = depth <= 0;
8324
+ const trimmed = stripLineComment(raw).trim();
8325
+ if (trimmed && !trimmed.startsWith("@@") && !trimmed.startsWith("}")) {
8326
+ const fm = trimmed.match(/^([A-Za-z_]\w*)\s+([A-Za-z_]\w*)/);
8327
+ if (fm && /@relation\b[^)]*\bfields\s*:/.test(trimmed)) {
8328
+ const parentTable = modelToTable.get(fm[2]);
8329
+ if (parentTable) {
8330
+ const key = `${current.table}->${parentTable}`;
8331
+ if (!seen.has(key)) {
8332
+ seen.add(key);
8333
+ out.push({
8334
+ childTable: current.table,
8335
+ parentTable,
8336
+ evidence: {
8337
+ file: path38.relative(serviceDir, file.path),
8338
+ line: lineNo,
8339
+ snippet: snippet(content, lineNo)
8340
+ }
8341
+ });
8342
+ }
8343
+ }
8344
+ }
8345
+ }
8346
+ if (closing) current = null;
8347
+ }
8348
+ return out;
8349
+ }
8350
+ async function prismaForeignKeys(serviceDir) {
8351
+ const schemaPath = await findFirst(serviceDir, [
8352
+ path38.join("prisma", "schema.prisma"),
8353
+ "schema.prisma"
8354
+ ]);
8355
+ if (!schemaPath) return [];
8356
+ const content = await readIfExists(schemaPath);
8357
+ if (!content) return [];
8358
+ return prismaForeignKeysFromSchema({ path: schemaPath, content }, serviceDir);
8359
+ }
8135
8360
 
8136
8361
  // src/extract/calls/go.ts
8137
8362
  import path39 from "path";
@@ -8306,16 +8531,80 @@ async function addCallEdges(graph, services) {
8306
8531
  };
8307
8532
  }
8308
8533
 
8534
+ // src/extract/table-edges.ts
8535
+ import {
8536
+ EdgeType as EdgeType15,
8537
+ NodeType as NodeType16,
8538
+ Provenance as Provenance15,
8539
+ confidenceForExtracted as confidenceForExtracted12,
8540
+ extractedEdgeId as extractedEdgeId9,
8541
+ infraId as infraId13
8542
+ } from "@neat.is/types";
8543
+ async function addTableEdges(graph, services) {
8544
+ let nodesAdded = 0;
8545
+ let edgesAdded = 0;
8546
+ for (const service of services) {
8547
+ const files = await loadSourceFiles(service.dir);
8548
+ const refs = [];
8549
+ for (const file of files) {
8550
+ try {
8551
+ refs.push(...drizzleForeignKeys(file, service.dir));
8552
+ refs.push(...sqlalchemyForeignKeys(file, service.dir));
8553
+ } catch (err) {
8554
+ recordExtractionError("foreign-key extraction", file.path, err);
8555
+ }
8556
+ }
8557
+ try {
8558
+ refs.push(...await prismaForeignKeys(service.dir));
8559
+ } catch (err) {
8560
+ recordExtractionError("prisma foreign-key extraction", service.dir, err);
8561
+ }
8562
+ for (const ref of refs) {
8563
+ const childId = infraId13("sql-table", ref.childTable);
8564
+ const parentId = infraId13("sql-table", ref.parentTable);
8565
+ if (childId === parentId) continue;
8566
+ nodesAdded += ensureTableNode(graph, childId, ref.childTable);
8567
+ nodesAdded += ensureTableNode(graph, parentId, ref.parentTable);
8568
+ const edgeId = extractedEdgeId9(childId, parentId, EdgeType15.REFERENCES);
8569
+ if (graph.hasEdge(edgeId)) continue;
8570
+ const edge = {
8571
+ id: edgeId,
8572
+ source: childId,
8573
+ target: parentId,
8574
+ type: EdgeType15.REFERENCES,
8575
+ provenance: Provenance15.EXTRACTED,
8576
+ confidence: confidenceForExtracted12("structural"),
8577
+ evidence: ref.evidence
8578
+ };
8579
+ graph.addEdgeWithKey(edgeId, childId, parentId, edge);
8580
+ edgesAdded++;
8581
+ }
8582
+ }
8583
+ return { nodesAdded, edgesAdded };
8584
+ }
8585
+ function ensureTableNode(graph, id, name) {
8586
+ if (graph.hasNode(id)) return 0;
8587
+ const node = {
8588
+ id,
8589
+ type: NodeType16.InfraNode,
8590
+ name,
8591
+ provider: "self",
8592
+ kind: "sql-table"
8593
+ };
8594
+ graph.addNode(id, node);
8595
+ return 1;
8596
+ }
8597
+
8309
8598
  // src/extract/infra/docker-compose.ts
8310
8599
  import path40 from "path";
8311
- import { EdgeType as EdgeType15, Provenance as Provenance16, confidenceForExtracted as confidenceForExtracted13 } from "@neat.is/types";
8600
+ import { EdgeType as EdgeType16, Provenance as Provenance17, confidenceForExtracted as confidenceForExtracted14 } from "@neat.is/types";
8312
8601
 
8313
8602
  // src/extract/infra/shared.ts
8314
- import { NodeType as NodeType16, Provenance as Provenance15, confidenceForExtracted as confidenceForExtracted12, infraId as infraId13 } from "@neat.is/types";
8603
+ import { NodeType as NodeType17, Provenance as Provenance16, confidenceForExtracted as confidenceForExtracted13, infraId as infraId14 } from "@neat.is/types";
8315
8604
  function makeInfraNode(kind, name, provider = "self", extras) {
8316
8605
  return {
8317
- id: infraId13(kind, name),
8318
- type: NodeType16.InfraNode,
8606
+ id: infraId14(kind, name),
8607
+ type: NodeType17.InfraNode,
8319
8608
  name,
8320
8609
  provider,
8321
8610
  kind,
@@ -8359,8 +8648,8 @@ function emitPlatformResourceEdge(graph, anchorId, edgeType, kind, name, provide
8359
8648
  source: anchorId,
8360
8649
  target: node.id,
8361
8650
  type: edgeType,
8362
- provenance: Provenance15.EXTRACTED,
8363
- confidence: confidenceForExtracted12("structural"),
8651
+ provenance: Provenance16.EXTRACTED,
8652
+ confidence: confidenceForExtracted13("structural"),
8364
8653
  evidence: { file: evidenceFile, ...line !== void 0 ? { line } : {} }
8365
8654
  };
8366
8655
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -8427,15 +8716,15 @@ async function addComposeInfra(graph, scanPath, services) {
8427
8716
  for (const dep of dependsOnList(svc.depends_on)) {
8428
8717
  const targetId = composeNameToNodeId.get(dep);
8429
8718
  if (!targetId) continue;
8430
- const edgeId = extractedEdgeId(sourceId, targetId, EdgeType15.DEPENDS_ON);
8719
+ const edgeId = extractedEdgeId(sourceId, targetId, EdgeType16.DEPENDS_ON);
8431
8720
  if (graph.hasEdge(edgeId)) continue;
8432
8721
  const edge = {
8433
8722
  id: edgeId,
8434
8723
  source: sourceId,
8435
8724
  target: targetId,
8436
- type: EdgeType15.DEPENDS_ON,
8437
- provenance: Provenance16.EXTRACTED,
8438
- confidence: confidenceForExtracted13("structural"),
8725
+ type: EdgeType16.DEPENDS_ON,
8726
+ provenance: Provenance17.EXTRACTED,
8727
+ confidence: confidenceForExtracted14("structural"),
8439
8728
  evidence: { file: evidenceFile }
8440
8729
  };
8441
8730
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -8448,7 +8737,7 @@ async function addComposeInfra(graph, scanPath, services) {
8448
8737
  // src/extract/infra/dockerfile.ts
8449
8738
  import path41 from "path";
8450
8739
  import { promises as fs17 } from "fs";
8451
- import { EdgeType as EdgeType16, Provenance as Provenance17, confidenceForExtracted as confidenceForExtracted14 } from "@neat.is/types";
8740
+ import { EdgeType as EdgeType17, Provenance as Provenance18, confidenceForExtracted as confidenceForExtracted15 } from "@neat.is/types";
8452
8741
  function readDockerfile(content) {
8453
8742
  let image = null;
8454
8743
  const ports = [];
@@ -8507,15 +8796,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
8507
8796
  );
8508
8797
  nodesAdded += fn;
8509
8798
  edgesAdded += fe;
8510
- const edgeId = extractedEdgeId(fileNodeId, node.id, EdgeType16.RUNS_ON);
8799
+ const edgeId = extractedEdgeId(fileNodeId, node.id, EdgeType17.RUNS_ON);
8511
8800
  if (!graph.hasEdge(edgeId)) {
8512
8801
  const edge = {
8513
8802
  id: edgeId,
8514
8803
  source: fileNodeId,
8515
8804
  target: node.id,
8516
- type: EdgeType16.RUNS_ON,
8517
- provenance: Provenance17.EXTRACTED,
8518
- confidence: confidenceForExtracted14("structural"),
8805
+ type: EdgeType17.RUNS_ON,
8806
+ provenance: Provenance18.EXTRACTED,
8807
+ confidence: confidenceForExtracted15("structural"),
8519
8808
  evidence: {
8520
8809
  file: evidenceFile,
8521
8810
  ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
@@ -8530,15 +8819,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
8530
8819
  graph.addNode(portNode.id, portNode);
8531
8820
  nodesAdded++;
8532
8821
  }
8533
- const portEdgeId = extractedEdgeId(fileNodeId, portNode.id, EdgeType16.CONNECTS_TO);
8822
+ const portEdgeId = extractedEdgeId(fileNodeId, portNode.id, EdgeType17.CONNECTS_TO);
8534
8823
  if (graph.hasEdge(portEdgeId)) continue;
8535
8824
  const portEdge = {
8536
8825
  id: portEdgeId,
8537
8826
  source: fileNodeId,
8538
8827
  target: portNode.id,
8539
- type: EdgeType16.CONNECTS_TO,
8540
- provenance: Provenance17.EXTRACTED,
8541
- confidence: confidenceForExtracted14("structural"),
8828
+ type: EdgeType17.CONNECTS_TO,
8829
+ provenance: Provenance18.EXTRACTED,
8830
+ confidence: confidenceForExtracted15("structural"),
8542
8831
  evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
8543
8832
  };
8544
8833
  graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
@@ -8551,7 +8840,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
8551
8840
  // src/extract/infra/terraform.ts
8552
8841
  import { promises as fs18 } from "fs";
8553
8842
  import path42 from "path";
8554
- import { EdgeType as EdgeType17, Provenance as Provenance18, confidenceForExtracted as confidenceForExtracted15 } from "@neat.is/types";
8843
+ import { EdgeType as EdgeType18, Provenance as Provenance19, confidenceForExtracted as confidenceForExtracted16 } from "@neat.is/types";
8555
8844
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
8556
8845
  var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
8557
8846
  async function walkTfFiles(start, depth = 0, max = 5) {
@@ -8632,16 +8921,16 @@ async function addTerraformResources(graph, scanPath) {
8632
8921
  if (!target) continue;
8633
8922
  if (seen.has(target.nodeId)) continue;
8634
8923
  seen.add(target.nodeId);
8635
- const edgeId = extractedEdgeId(resource.nodeId, target.nodeId, EdgeType17.DEPENDS_ON);
8924
+ const edgeId = extractedEdgeId(resource.nodeId, target.nodeId, EdgeType18.DEPENDS_ON);
8636
8925
  if (graph.hasEdge(edgeId)) continue;
8637
8926
  const line = lineAt2(content, resource.bodyOffset + ref.index);
8638
8927
  const edge = {
8639
8928
  id: edgeId,
8640
8929
  source: resource.nodeId,
8641
8930
  target: target.nodeId,
8642
- type: EdgeType17.DEPENDS_ON,
8643
- provenance: Provenance18.EXTRACTED,
8644
- confidence: confidenceForExtracted15("structural"),
8931
+ type: EdgeType18.DEPENDS_ON,
8932
+ provenance: Provenance19.EXTRACTED,
8933
+ confidence: confidenceForExtracted16("structural"),
8645
8934
  evidence: { file: evidenceFile, line, snippet: key }
8646
8935
  };
8647
8936
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -8711,7 +9000,7 @@ async function addK8sResources(graph, scanPath) {
8711
9000
  import { promises as fs20 } from "fs";
8712
9001
  import path44 from "path";
8713
9002
  import { parse as parseToml2 } from "smol-toml";
8714
- import { EdgeType as EdgeType18, Provenance as Provenance19, confidenceForExtracted as confidenceForExtracted16 } from "@neat.is/types";
9003
+ import { EdgeType as EdgeType19, Provenance as Provenance20, confidenceForExtracted as confidenceForExtracted17 } from "@neat.is/types";
8715
9004
  var WRANGLER_FILENAMES = ["wrangler.toml", "wrangler.jsonc", "wrangler.json"];
8716
9005
  async function readWranglerConfig(dir) {
8717
9006
  for (const filename of WRANGLER_FILENAMES) {
@@ -8759,8 +9048,8 @@ function addResourceEdge(graph, anchorId, edgeType, kind, name, evidenceFile, li
8759
9048
  source: anchorId,
8760
9049
  target: node.id,
8761
9050
  type: edgeType,
8762
- provenance: Provenance19.EXTRACTED,
8763
- confidence: confidenceForExtracted16("structural"),
9051
+ provenance: Provenance20.EXTRACTED,
9052
+ confidence: confidenceForExtracted17("structural"),
8764
9053
  evidence: { file: evidenceFile, ...line !== void 0 ? { line } : {} }
8765
9054
  };
8766
9055
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -8821,15 +9110,15 @@ async function addCloudflareWorkers(graph, services, scanPath) {
8821
9110
  nodesAdded++;
8822
9111
  }
8823
9112
  if (runtimeNode.id !== anchorId) {
8824
- const runsOnId = extractedEdgeId(anchorId, runtimeNode.id, EdgeType18.RUNS_ON);
9113
+ const runsOnId = extractedEdgeId(anchorId, runtimeNode.id, EdgeType19.RUNS_ON);
8825
9114
  if (!graph.hasEdge(runsOnId)) {
8826
9115
  const edge = {
8827
9116
  id: runsOnId,
8828
9117
  source: anchorId,
8829
9118
  target: runtimeNode.id,
8830
- type: EdgeType18.RUNS_ON,
8831
- provenance: Provenance19.EXTRACTED,
8832
- confidence: confidenceForExtracted16("structural"),
9119
+ type: EdgeType19.RUNS_ON,
9120
+ provenance: Provenance20.EXTRACTED,
9121
+ confidence: confidenceForExtracted17("structural"),
8833
9122
  evidence: {
8834
9123
  file: evidenceFile,
8835
9124
  ...config.compatibility_date ? { snippet: `compatibility_date = ${config.compatibility_date}`.slice(0, 120) } : {}
@@ -8843,7 +9132,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
8843
9132
  const result = addResourceEdge(
8844
9133
  graph,
8845
9134
  anchorId,
8846
- EdgeType18.CONNECTS_TO,
9135
+ EdgeType19.CONNECTS_TO,
8847
9136
  "cloudflare-route",
8848
9137
  route,
8849
9138
  evidenceFile,
@@ -8867,7 +9156,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
8867
9156
  const result = addResourceEdge(
8868
9157
  graph,
8869
9158
  anchorId,
8870
- EdgeType18.DEPENDS_ON,
9159
+ EdgeType19.DEPENDS_ON,
8871
9160
  group.kind,
8872
9161
  name,
8873
9162
  evidenceFile,
@@ -8881,7 +9170,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
8881
9170
  const result = addResourceEdge(
8882
9171
  graph,
8883
9172
  anchorId,
8884
- EdgeType18.DEPENDS_ON,
9173
+ EdgeType19.DEPENDS_ON,
8885
9174
  "cloudflare-cron",
8886
9175
  cron,
8887
9176
  evidenceFile,
@@ -8894,7 +9183,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
8894
9183
  const result = addResourceEdge(
8895
9184
  graph,
8896
9185
  anchorId,
8897
- EdgeType18.DEPENDS_ON,
9186
+ EdgeType19.DEPENDS_ON,
8898
9187
  "cloudflare-env-var",
8899
9188
  varName,
8900
9189
  evidenceFile,
@@ -8907,15 +9196,15 @@ async function addCloudflareWorkers(graph, services, scanPath) {
8907
9196
  if (!svc.service) continue;
8908
9197
  const target = workerIndex.get(svc.service);
8909
9198
  if (target && target.anchorId !== anchorId) {
8910
- const edgeId = extractedEdgeId(anchorId, target.anchorId, EdgeType18.CALLS);
9199
+ const edgeId = extractedEdgeId(anchorId, target.anchorId, EdgeType19.CALLS);
8911
9200
  if (!graph.hasEdge(edgeId)) {
8912
9201
  const edge = {
8913
9202
  id: edgeId,
8914
9203
  source: anchorId,
8915
9204
  target: target.anchorId,
8916
- type: EdgeType18.CALLS,
8917
- provenance: Provenance19.EXTRACTED,
8918
- confidence: confidenceForExtracted16("structural"),
9205
+ type: EdgeType19.CALLS,
9206
+ provenance: Provenance20.EXTRACTED,
9207
+ confidence: confidenceForExtracted17("structural"),
8919
9208
  evidence: { file: evidenceFile, line: lineContaining2(raw, svc.service) }
8920
9209
  };
8921
9210
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -8926,7 +9215,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
8926
9215
  const result = addResourceEdge(
8927
9216
  graph,
8928
9217
  anchorId,
8929
- EdgeType18.DEPENDS_ON,
9218
+ EdgeType19.DEPENDS_ON,
8930
9219
  "cloudflare-service-binding",
8931
9220
  svc.service,
8932
9221
  evidenceFile,
@@ -8942,7 +9231,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
8942
9231
  // src/extract/infra/vercel.ts
8943
9232
  import { promises as fs21 } from "fs";
8944
9233
  import path45 from "path";
8945
- import { EdgeType as EdgeType19 } from "@neat.is/types";
9234
+ import { EdgeType as EdgeType20 } from "@neat.is/types";
8946
9235
  var VERCEL_CONFIG_FILENAMES = ["vercel.json", "vercel.jsonc"];
8947
9236
  async function readVercelConfig(dir) {
8948
9237
  for (const filename of VERCEL_CONFIG_FILENAMES) {
@@ -9005,12 +9294,12 @@ async function addVercelServices(graph, services, scanPath) {
9005
9294
  nodesAdded += result.nodesAdded;
9006
9295
  edgesAdded += result.edgesAdded;
9007
9296
  };
9008
- add(EdgeType19.RUNS_ON, "vercel", "vercel");
9009
- for (const cron of config.crons ?? []) add(EdgeType19.DEPENDS_ON, "vercel-cron", cron.path ?? cron.schedule);
9010
- for (const varName of Object.keys(config.env ?? {})) add(EdgeType19.DEPENDS_ON, "vercel-env-var", varName);
9011
- for (const varName of Object.keys(config.build?.env ?? {})) add(EdgeType19.DEPENDS_ON, "vercel-env-var", varName);
9297
+ add(EdgeType20.RUNS_ON, "vercel", "vercel");
9298
+ for (const cron of config.crons ?? []) add(EdgeType20.DEPENDS_ON, "vercel-cron", cron.path ?? cron.schedule);
9299
+ for (const varName of Object.keys(config.env ?? {})) add(EdgeType20.DEPENDS_ON, "vercel-env-var", varName);
9300
+ for (const varName of Object.keys(config.build?.env ?? {})) add(EdgeType20.DEPENDS_ON, "vercel-env-var", varName);
9012
9301
  for (const route of [...config.rewrites ?? [], ...config.redirects ?? [], ...config.routes ?? []]) {
9013
- add(EdgeType19.CONNECTS_TO, "vercel-route", routeSource(route));
9302
+ add(EdgeType20.CONNECTS_TO, "vercel-route", routeSource(route));
9014
9303
  }
9015
9304
  }
9016
9305
  return { nodesAdded, edgesAdded };
@@ -9020,7 +9309,7 @@ async function addVercelServices(graph, services, scanPath) {
9020
9309
  import { promises as fs22 } from "fs";
9021
9310
  import path46 from "path";
9022
9311
  import { parse as parseToml3 } from "smol-toml";
9023
- import { EdgeType as EdgeType20 } from "@neat.is/types";
9312
+ import { EdgeType as EdgeType21 } from "@neat.is/types";
9024
9313
  var RAILWAY_FILENAMES = ["railway.toml", "railway.json", "railway.jsonc"];
9025
9314
  async function readRailwayConfig(dir) {
9026
9315
  for (const filename of RAILWAY_FILENAMES) {
@@ -9066,9 +9355,9 @@ async function addRailwayServices(graph, services, scanPath) {
9066
9355
  nodesAdded += result.nodesAdded;
9067
9356
  edgesAdded += result.edgesAdded;
9068
9357
  };
9069
- add(EdgeType20.RUNS_ON, "railway", "railway");
9070
- add(EdgeType20.CONNECTS_TO, "railway-route", config.deploy?.healthcheckPath);
9071
- add(EdgeType20.DEPENDS_ON, "railway-cron", config.deploy?.cronSchedule);
9358
+ add(EdgeType21.RUNS_ON, "railway", "railway");
9359
+ add(EdgeType21.CONNECTS_TO, "railway-route", config.deploy?.healthcheckPath);
9360
+ add(EdgeType21.DEPENDS_ON, "railway-cron", config.deploy?.cronSchedule);
9072
9361
  }
9073
9362
  return { nodesAdded, edgesAdded };
9074
9363
  }
@@ -9077,7 +9366,7 @@ async function addRailwayServices(graph, services, scanPath) {
9077
9366
  import { promises as fs23 } from "fs";
9078
9367
  import path47 from "path";
9079
9368
  import { parse as parseToml4 } from "smol-toml";
9080
- import { EdgeType as EdgeType21 } from "@neat.is/types";
9369
+ import { EdgeType as EdgeType22 } from "@neat.is/types";
9081
9370
  async function readSupabaseConfig(dir) {
9082
9371
  const relFile = path47.join("supabase", "config.toml");
9083
9372
  const abs = path47.join(dir, relFile);
@@ -9125,10 +9414,10 @@ async function addSupabaseProjects(graph, services, scanPath) {
9125
9414
  nodesAdded += result.nodesAdded;
9126
9415
  edgesAdded += result.edgesAdded;
9127
9416
  };
9128
- add(EdgeType21.RUNS_ON, "supabase", "supabase");
9129
- for (const fn of Object.keys(config.functions ?? {})) add(EdgeType21.DEPENDS_ON, "supabase-function", fn);
9130
- if (config.storage) add(EdgeType21.DEPENDS_ON, "supabase-storage", "storage");
9131
- if (config.auth) add(EdgeType21.DEPENDS_ON, "supabase-auth", "auth");
9417
+ add(EdgeType22.RUNS_ON, "supabase", "supabase");
9418
+ for (const fn of Object.keys(config.functions ?? {})) add(EdgeType22.DEPENDS_ON, "supabase-function", fn);
9419
+ if (config.storage) add(EdgeType22.DEPENDS_ON, "supabase-storage", "storage");
9420
+ if (config.auth) add(EdgeType22.DEPENDS_ON, "supabase-auth", "auth");
9132
9421
  }
9133
9422
  return { nodesAdded, edgesAdded };
9134
9423
  }
@@ -9155,11 +9444,11 @@ import path49 from "path";
9155
9444
  // src/extract/retire.ts
9156
9445
  import { existsSync as existsSync2 } from "fs";
9157
9446
  import path48 from "path";
9158
- import { NodeType as NodeType17, Provenance as Provenance20 } from "@neat.is/types";
9447
+ import { NodeType as NodeType18, Provenance as Provenance21 } from "@neat.is/types";
9159
9448
  function dropOrphanedFileNodes(graph) {
9160
9449
  const orphans = [];
9161
9450
  graph.forEachNode((id, attrs) => {
9162
- if (attrs.type !== NodeType17.FileNode) return;
9451
+ if (attrs.type !== NodeType18.FileNode) return;
9163
9452
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
9164
9453
  orphans.push(id);
9165
9454
  }
@@ -9172,7 +9461,7 @@ function retireEdgesByFile(graph, file) {
9172
9461
  const toDrop = [];
9173
9462
  graph.forEachEdge((id, attrs) => {
9174
9463
  const edge = attrs;
9175
- if (edge.provenance !== Provenance20.EXTRACTED) return;
9464
+ if (edge.provenance !== Provenance21.EXTRACTED) return;
9176
9465
  if (!edge.evidence?.file) return;
9177
9466
  if (edge.evidence.file === normalized) toDrop.push(id);
9178
9467
  });
@@ -9185,7 +9474,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
9185
9474
  const bases = [scanPath, ...serviceDirs];
9186
9475
  graph.forEachEdge((id, attrs) => {
9187
9476
  const edge = attrs;
9188
- if (edge.provenance !== Provenance20.EXTRACTED) return;
9477
+ if (edge.provenance !== Provenance21.EXTRACTED) return;
9189
9478
  const evidenceFile = edge.evidence?.file;
9190
9479
  if (!evidenceFile) return;
9191
9480
  if (path48.isAbsolute(evidenceFile)) {
@@ -9216,6 +9505,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
9216
9505
  const routePhase = await addRoutes(graph, services);
9217
9506
  const grpcPhase = await addGrpcMethods(graph, services);
9218
9507
  const phase4 = await addCallEdges(graph, services);
9508
+ const tableEdges = await addTableEdges(graph, services);
9219
9509
  const phase5 = await addInfra(graph, scanPath, services);
9220
9510
  const ghostsRetired = retireExtractedEdgesByMissingFile(
9221
9511
  graph,
@@ -9255,8 +9545,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
9255
9545
  }
9256
9546
  }
9257
9547
  const result = {
9258
- nodesAdded: phase1Nodes + fileEnum.nodesAdded + symbolEnum.nodesAdded + importGraph.nodesAdded + symbolEdges.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
9259
- edgesAdded: fileEnum.edgesAdded + symbolEnum.edgesAdded + importGraph.edgesAdded + symbolEdges.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
9548
+ nodesAdded: phase1Nodes + fileEnum.nodesAdded + symbolEnum.nodesAdded + importGraph.nodesAdded + symbolEdges.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + tableEdges.nodesAdded + phase5.nodesAdded,
9549
+ edgesAdded: fileEnum.edgesAdded + symbolEnum.edgesAdded + importGraph.edgesAdded + symbolEdges.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + tableEdges.edgesAdded + phase5.edgesAdded,
9260
9550
  frontiersPromoted,
9261
9551
  extractionErrors: errorEntries.length,
9262
9552
  errorEntries,
@@ -9281,22 +9571,22 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
9281
9571
  import {
9282
9572
  databaseId as databaseId3,
9283
9573
  DivergenceResultSchema,
9284
- EdgeType as EdgeType22,
9285
- NodeType as NodeType18,
9574
+ EdgeType as EdgeType23,
9575
+ NodeType as NodeType19,
9286
9576
  parseEdgeId,
9287
9577
  parseFileId,
9288
- Provenance as Provenance21,
9578
+ Provenance as Provenance22,
9289
9579
  serviceId as serviceId5
9290
9580
  } from "@neat.is/types";
9291
9581
  function bucketKey(source, target, type) {
9292
9582
  return `${type}|${source}|${target}`;
9293
9583
  }
9294
9584
  function bucketSourceFor(graph, edge) {
9295
- if (edge.type !== EdgeType22.CONNECTS_TO) return edge.source;
9585
+ if (edge.type !== EdgeType23.CONNECTS_TO) return edge.source;
9296
9586
  const parsed = parseFileId(edge.source);
9297
9587
  if (!parsed || !graph.hasNode(edge.target)) return edge.source;
9298
9588
  const target = graph.getNodeAttributes(edge.target);
9299
- if (target.type !== NodeType18.DatabaseNode) return edge.source;
9589
+ if (target.type !== NodeType19.DatabaseNode) return edge.source;
9300
9590
  return serviceId5(parsed.service);
9301
9591
  }
9302
9592
  function bucketEdges(graph) {
@@ -9309,17 +9599,17 @@ function bucketEdges(graph) {
9309
9599
  const key = bucketKey(source, e.target, e.type);
9310
9600
  const cur = buckets2.get(key) ?? { source, target: e.target, type: e.type };
9311
9601
  switch (provenance) {
9312
- case Provenance21.EXTRACTED:
9602
+ case Provenance22.EXTRACTED:
9313
9603
  cur.extracted = e;
9314
9604
  break;
9315
- case Provenance21.OBSERVED:
9605
+ case Provenance22.OBSERVED:
9316
9606
  cur.observed = e;
9317
9607
  break;
9318
- case Provenance21.INFERRED:
9608
+ case Provenance22.INFERRED:
9319
9609
  cur.inferred = e;
9320
9610
  break;
9321
9611
  default:
9322
- if (e.provenance === Provenance21.STALE) cur.stale = e;
9612
+ if (e.provenance === Provenance22.STALE) cur.stale = e;
9323
9613
  }
9324
9614
  buckets2.set(key, cur);
9325
9615
  });
@@ -9328,17 +9618,17 @@ function bucketEdges(graph) {
9328
9618
  function nodeIsFrontier(graph, nodeId) {
9329
9619
  if (!graph.hasNode(nodeId)) return false;
9330
9620
  const attrs = graph.getNodeAttributes(nodeId);
9331
- return attrs.type === NodeType18.FrontierNode;
9621
+ return attrs.type === NodeType19.FrontierNode;
9332
9622
  }
9333
9623
  function nodeIsWebsocketChannel(graph, nodeId) {
9334
9624
  if (!graph.hasNode(nodeId)) return false;
9335
9625
  const attrs = graph.getNodeAttributes(nodeId);
9336
- return attrs.type === NodeType18.WebSocketChannelNode;
9626
+ return attrs.type === NodeType19.WebSocketChannelNode;
9337
9627
  }
9338
9628
  function nodeIsSymbol(graph, nodeId) {
9339
9629
  if (!graph.hasNode(nodeId)) return false;
9340
9630
  const attrs = graph.getNodeAttributes(nodeId);
9341
- return attrs.type === NodeType18.SymbolNode;
9631
+ return attrs.type === NodeType19.SymbolNode;
9342
9632
  }
9343
9633
  function clampConfidence(n) {
9344
9634
  if (!Number.isFinite(n)) return 0;
@@ -9358,14 +9648,14 @@ function gradedConfidence(edge) {
9358
9648
  return clampConfidence(confidenceForEdge(edge));
9359
9649
  }
9360
9650
  var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
9361
- EdgeType22.CALLS,
9362
- EdgeType22.CONNECTS_TO,
9363
- EdgeType22.PUBLISHES_TO,
9364
- EdgeType22.CONSUMES_FROM
9651
+ EdgeType23.CALLS,
9652
+ EdgeType23.CONNECTS_TO,
9653
+ EdgeType23.PUBLISHES_TO,
9654
+ EdgeType23.CONSUMES_FROM
9365
9655
  ]);
9366
9656
  function detectMissingDivergences(graph, bucket) {
9367
9657
  const out = [];
9368
- if (bucket.type === EdgeType22.CONTAINS) return out;
9658
+ if (bucket.type === EdgeType23.CONTAINS) return out;
9369
9659
  if (nodeIsSymbol(graph, bucket.source) || nodeIsSymbol(graph, bucket.target)) return out;
9370
9660
  if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
9371
9661
  if (!nodeIsFrontier(graph, bucket.target)) {
@@ -9407,7 +9697,7 @@ function declaredHostFor(svc) {
9407
9697
  function hasExtractedConfiguredBy(graph, svcId) {
9408
9698
  for (const edgeId of graph.outboundEdges(svcId)) {
9409
9699
  const e = graph.getEdgeAttributes(edgeId);
9410
- if (e.type === EdgeType22.CONFIGURED_BY && e.provenance === Provenance21.EXTRACTED) {
9700
+ if (e.type === EdgeType23.CONFIGURED_BY && e.provenance === Provenance22.EXTRACTED) {
9411
9701
  return true;
9412
9702
  }
9413
9703
  }
@@ -9420,10 +9710,10 @@ function detectHostMismatch(graph, svcId, svc) {
9420
9710
  const out = [];
9421
9711
  for (const edgeId of graph.outboundEdges(svcId)) {
9422
9712
  const edge = graph.getEdgeAttributes(edgeId);
9423
- if (edge.type !== EdgeType22.CONNECTS_TO) continue;
9424
- if (edge.provenance !== Provenance21.OBSERVED) continue;
9713
+ if (edge.type !== EdgeType23.CONNECTS_TO) continue;
9714
+ if (edge.provenance !== Provenance22.OBSERVED) continue;
9425
9715
  const target = graph.getNodeAttributes(edge.target);
9426
- if (target.type !== NodeType18.DatabaseNode) continue;
9716
+ if (target.type !== NodeType19.DatabaseNode) continue;
9427
9717
  const observedHost = target.host?.trim();
9428
9718
  if (!observedHost) continue;
9429
9719
  if (observedHost === declaredHost) continue;
@@ -9445,10 +9735,10 @@ function detectCompatDivergences(graph, svcId, svc) {
9445
9735
  const deps = svc.dependencies ?? {};
9446
9736
  for (const edgeId of graph.outboundEdges(svcId)) {
9447
9737
  const edge = graph.getEdgeAttributes(edgeId);
9448
- if (edge.type !== EdgeType22.CONNECTS_TO) continue;
9449
- if (edge.provenance !== Provenance21.OBSERVED) continue;
9738
+ if (edge.type !== EdgeType23.CONNECTS_TO) continue;
9739
+ if (edge.provenance !== Provenance22.OBSERVED) continue;
9450
9740
  const target = graph.getNodeAttributes(edge.target);
9451
- if (target.type !== NodeType18.DatabaseNode) continue;
9741
+ if (target.type !== NodeType19.DatabaseNode) continue;
9452
9742
  for (const pair of compatPairs()) {
9453
9743
  if (pair.engine !== target.engine) continue;
9454
9744
  const declared = deps[pair.driver];
@@ -9564,13 +9854,13 @@ function computeDivergences(graph, opts = {}) {
9564
9854
  }
9565
9855
  graph.forEachNode((nodeId, attrs) => {
9566
9856
  const n = attrs;
9567
- if (n.type === NodeType18.ServiceNode) {
9857
+ if (n.type === NodeType19.ServiceNode) {
9568
9858
  const svc = n;
9569
9859
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
9570
9860
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
9571
9861
  return;
9572
9862
  }
9573
- if (n.type === NodeType18.InfraNode && n.kind === "sql-table") {
9863
+ if (n.type === NodeType19.InfraNode && n.kind === "sql-table") {
9574
9864
  for (const d of detectColumnDrift(n)) all.push(d);
9575
9865
  }
9576
9866
  });
@@ -9616,7 +9906,7 @@ function computeDivergences(graph, opts = {}) {
9616
9906
  // src/persist.ts
9617
9907
  import { promises as fs24 } from "fs";
9618
9908
  import path50 from "path";
9619
- import { NodeType as NodeType19, Provenance as Provenance22, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
9909
+ import { NodeType as NodeType20, Provenance as Provenance23, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
9620
9910
  var SCHEMA_VERSION = 6;
9621
9911
  function migrateV1ToV2(payload) {
9622
9912
  const nodes = payload.graph.nodes;
@@ -9640,7 +9930,7 @@ function migrateV5ToV6(payload) {
9640
9930
  if (Array.isArray(nodes)) {
9641
9931
  for (const node of nodes) {
9642
9932
  const attrs = node.attributes;
9643
- if (!attrs || attrs.type !== NodeType19.InfraNode) continue;
9933
+ if (!attrs || attrs.type !== NodeType20.InfraNode) continue;
9644
9934
  if (attrs.kind !== "sql-table" && attrs.kind !== "supabase-table") continue;
9645
9935
  if (!Array.isArray(attrs.columns)) attrs.columns = [];
9646
9936
  }
@@ -9653,7 +9943,7 @@ function migrateV2ToV3(payload) {
9653
9943
  for (const edge of edges) {
9654
9944
  const attrs = edge.attributes;
9655
9945
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
9656
- attrs.provenance = Provenance22.OBSERVED;
9946
+ attrs.provenance = Provenance23.OBSERVED;
9657
9947
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
9658
9948
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
9659
9949
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
@@ -11051,14 +11341,14 @@ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_
11051
11341
  }
11052
11342
 
11053
11343
  // src/connectors/index.ts
11054
- import { NodeType as NodeType20, parseFileId as parseFileId2, Provenance as Provenance23 } from "@neat.is/types";
11344
+ import { NodeType as NodeType21, parseFileId as parseFileId2, Provenance as Provenance24 } from "@neat.is/types";
11055
11345
  var NO_ENV = "unknown";
11056
11346
  function staticCallSiteFor(graph, serviceName, targetNodeId) {
11057
11347
  if (!graph.hasNode(targetNodeId)) return void 0;
11058
11348
  const sites = [];
11059
11349
  for (const edgeId of graph.inboundEdges(targetNodeId)) {
11060
11350
  const edge = graph.getEdgeAttributes(edgeId);
11061
- if (edge.provenance !== Provenance23.EXTRACTED) continue;
11351
+ if (edge.provenance !== Provenance24.EXTRACTED) continue;
11062
11352
  const parsed = parseFileId2(edge.source);
11063
11353
  if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
11064
11354
  const site = { relPath: edge.evidence.file };
@@ -11070,7 +11360,7 @@ function staticCallSiteFor(graph, serviceName, targetNodeId) {
11070
11360
  function routeCallSiteFor(graph, targetNodeId) {
11071
11361
  if (!graph.hasNode(targetNodeId)) return void 0;
11072
11362
  const attrs = graph.getNodeAttributes(targetNodeId);
11073
- if (attrs.type !== NodeType20.RouteNode || !attrs.path) return void 0;
11363
+ if (attrs.type !== NodeType21.RouteNode || !attrs.path) return void 0;
11074
11364
  const site = { relPath: attrs.path };
11075
11365
  if (attrs.line !== void 0) site.line = attrs.line;
11076
11366
  return site;
@@ -11250,7 +11540,11 @@ var JUNCTION_DEFAULT_RATE_LIMITS = {
11250
11540
  // connector add/remove/test` (provision/deprovision/validate), never a poll
11251
11541
  // loop, so this bucket is exercised a handful of times per command. Kept
11252
11542
  // conservative pending a documented Drains-API rate limit.
11253
- vercel: { capacity: 20, refillMs: 5e3 }
11543
+ vercel: { capacity: 20, refillMs: 5e3 },
11544
+ // Render's REST API (api-docs.render.com/reference/rate-limiting) isn't
11545
+ // pinned here to a single confirmed number — this is a conservative
11546
+ // placeholder pending a live project, matching the other pull providers.
11547
+ render: { capacity: 30, refillMs: 1e4 }
11254
11548
  };
11255
11549
  var JUNCTION_GENERIC_RATE_LIMIT = { capacity: 20, refillMs: 5e3 };
11256
11550
  function defaultRateLimitFor(provider) {
@@ -11646,23 +11940,23 @@ async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT
11646
11940
  }
11647
11941
 
11648
11942
  // src/connectors/supabase/resolve.ts
11649
- import { EdgeType as EdgeType23, infraId as infraId14 } from "@neat.is/types";
11943
+ import { EdgeType as EdgeType24, infraId as infraId15 } from "@neat.is/types";
11650
11944
  function createSupabaseResolveTarget(graph, config) {
11651
11945
  return (signal, _ctx) => {
11652
11946
  if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
11653
11947
  return null;
11654
11948
  }
11655
- const subResourceId = infraId14(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
11949
+ const subResourceId = infraId15(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
11656
11950
  if (graph.hasNode(subResourceId)) {
11657
- return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: EdgeType23.CALLS };
11951
+ return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: EdgeType24.CALLS };
11658
11952
  }
11659
- const bareResourceId = infraId14(signal.targetKind, signal.targetName);
11953
+ const bareResourceId = infraId15(signal.targetKind, signal.targetName);
11660
11954
  if (graph.hasNode(bareResourceId)) {
11661
- return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: EdgeType23.CALLS };
11955
+ return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: EdgeType24.CALLS };
11662
11956
  }
11663
- const projectLevelId = infraId14("supabase", config.nodeRef);
11957
+ const projectLevelId = infraId15("supabase", config.nodeRef);
11664
11958
  if (graph.hasNode(projectLevelId)) {
11665
- return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: EdgeType23.CALLS };
11959
+ return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: EdgeType24.CALLS };
11666
11960
  }
11667
11961
  return null;
11668
11962
  };
@@ -11754,7 +12048,7 @@ function createSupabaseConnector(graph, config, deps = {}) {
11754
12048
  }
11755
12049
 
11756
12050
  // src/connectors/railway/index.ts
11757
- import { EdgeType as EdgeType24, NodeType as NodeType21, serviceId as serviceId6 } from "@neat.is/types";
12051
+ import { EdgeType as EdgeType25, NodeType as NodeType22, serviceId as serviceId6 } from "@neat.is/types";
11758
12052
 
11759
12053
  // src/connectors/railway/client.ts
11760
12054
  var DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
@@ -11904,7 +12198,7 @@ function buildRailwayRouteIndex(graph, serviceName) {
11904
12198
  const out = [];
11905
12199
  graph.forEachNode((_id, attrs) => {
11906
12200
  const node = attrs;
11907
- if (node.type !== NodeType21.RouteNode) return;
12201
+ if (node.type !== NodeType22.RouteNode) return;
11908
12202
  const route = attrs;
11909
12203
  if (route.service !== serviceName) return;
11910
12204
  out.push({
@@ -12008,12 +12302,12 @@ function createRailwayResolveTarget(config) {
12008
12302
  const serviceName = config.serviceNameById[config.serviceId];
12009
12303
  if (!serviceName) return null;
12010
12304
  if (signal.targetKind === ROUTE_TARGET_KIND) {
12011
- return { targetNodeId: signal.targetName, serviceName, edgeType: EdgeType24.CALLS };
12305
+ return { targetNodeId: signal.targetName, serviceName, edgeType: EdgeType25.CALLS };
12012
12306
  }
12013
12307
  if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
12014
12308
  const peerName = config.serviceNameById[signal.targetName];
12015
12309
  if (!peerName) return null;
12016
- return { targetNodeId: serviceId6(peerName), serviceName, edgeType: EdgeType24.CONNECTS_TO };
12310
+ return { targetNodeId: serviceId6(peerName), serviceName, edgeType: EdgeType25.CONNECTS_TO };
12017
12311
  }
12018
12312
  return null;
12019
12313
  };
@@ -12195,7 +12489,7 @@ function mapLogEntriesToSignals(entries) {
12195
12489
  }
12196
12490
 
12197
12491
  // src/connectors/firebase/resolve.ts
12198
- import { NodeType as NodeType22, EdgeType as EdgeType25 } from "@neat.is/types";
12492
+ import { NodeType as NodeType23, EdgeType as EdgeType26 } from "@neat.is/types";
12199
12493
  function neatServiceNameFor(resourceType, resourceName, serviceMap) {
12200
12494
  switch (resourceType) {
12201
12495
  case "cloud_function":
@@ -12210,7 +12504,7 @@ function routeEntriesFor(graph, serviceName) {
12210
12504
  const entries = [];
12211
12505
  graph.forEachNode((_id, attrs) => {
12212
12506
  const node = attrs;
12213
- if (node.type !== NodeType22.RouteNode) return;
12507
+ if (node.type !== NodeType23.RouteNode) return;
12214
12508
  const route = attrs;
12215
12509
  if (route.service !== serviceName) return;
12216
12510
  entries.push({
@@ -12242,7 +12536,7 @@ function createFirebaseResolveTarget(graph, serviceMap) {
12242
12536
  return {
12243
12537
  targetNodeId: match.routeNodeId,
12244
12538
  serviceName,
12245
- edgeType: EdgeType25.CALLS
12539
+ edgeType: EdgeType26.CALLS
12246
12540
  };
12247
12541
  };
12248
12542
  }
@@ -12265,7 +12559,7 @@ function createFirebaseConnector(graph, serviceMap) {
12265
12559
  }
12266
12560
 
12267
12561
  // src/connectors/cloudflare/connector.ts
12268
- import { EdgeType as EdgeType26, NodeType as NodeType23, fileId as fileId4, infraId as infraId15 } from "@neat.is/types";
12562
+ import { EdgeType as EdgeType27, NodeType as NodeType24, fileId as fileId4, infraId as infraId16 } from "@neat.is/types";
12269
12563
 
12270
12564
  // src/connectors/cloudflare/client.ts
12271
12565
  import { randomUUID } from "crypto";
@@ -12424,7 +12718,7 @@ function findTaggedWorkerFileNode(graph, workerName) {
12424
12718
  graph.forEachNode((id, attrs) => {
12425
12719
  if (found) return;
12426
12720
  const a = attrs;
12427
- if (a.type === NodeType23.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
12721
+ if (a.type === NodeType24.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
12428
12722
  found = id;
12429
12723
  }
12430
12724
  });
@@ -12436,7 +12730,7 @@ function findMatchingRouteNode(graph, serviceName, method, path56) {
12436
12730
  graph.forEachNode((id, attrs) => {
12437
12731
  if (found) return;
12438
12732
  const a = attrs;
12439
- if (a.type !== NodeType23.RouteNode || a.service !== serviceName) return;
12733
+ if (a.type !== NodeType24.RouteNode || a.service !== serviceName) return;
12440
12734
  if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
12441
12735
  const routeMethod = (a.method ?? "").toUpperCase();
12442
12736
  if (routeMethod !== "ALL" && routeMethod !== method) return;
@@ -12459,7 +12753,7 @@ function createCloudflareResolveTarget(config, graph) {
12459
12753
  return {
12460
12754
  targetNodeId: resolveRouteGrain(mapping.service, wholeFileId),
12461
12755
  serviceName: mapping.service,
12462
- edgeType: EdgeType26.CALLS
12756
+ edgeType: EdgeType27.CALLS
12463
12757
  };
12464
12758
  }
12465
12759
  const taggedFileId = findTaggedWorkerFileNode(graph, scriptName);
@@ -12468,13 +12762,13 @@ function createCloudflareResolveTarget(config, graph) {
12468
12762
  return {
12469
12763
  targetNodeId: resolveRouteGrain(fileNode.service, taggedFileId),
12470
12764
  serviceName: fileNode.service,
12471
- edgeType: EdgeType26.CALLS
12765
+ edgeType: EdgeType27.CALLS
12472
12766
  };
12473
12767
  }
12474
12768
  return {
12475
- targetNodeId: infraId15("cloudflare-worker", scriptName),
12769
+ targetNodeId: infraId16("cloudflare-worker", scriptName),
12476
12770
  serviceName: scriptName,
12477
- edgeType: EdgeType26.CALLS,
12771
+ edgeType: EdgeType27.CALLS,
12478
12772
  ensureInfraNode: { kind: "cloudflare-worker", name: scriptName, provider: "cloudflare" }
12479
12773
  };
12480
12774
  };
@@ -12656,14 +12950,14 @@ function diffNeonStatementsToSignals(rows, previous, observedAtIso) {
12656
12950
  }
12657
12951
 
12658
12952
  // src/connectors/neon/resolve.ts
12659
- import { EdgeType as EdgeType27, infraId as infraId16 } from "@neat.is/types";
12953
+ import { EdgeType as EdgeType28, infraId as infraId17 } from "@neat.is/types";
12660
12954
  function createNeonResolveTarget(config) {
12661
12955
  return (signal) => {
12662
12956
  if (signal.targetKind !== NEON_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
12663
12957
  return {
12664
- targetNodeId: infraId16("sql-table", signal.targetName),
12958
+ targetNodeId: infraId17("sql-table", signal.targetName),
12665
12959
  serviceName: config.serviceName,
12666
- edgeType: EdgeType27.CALLS,
12960
+ edgeType: EdgeType28.CALLS,
12667
12961
  ensureInfraNode: { kind: "sql-table", name: signal.targetName, provider: "neon" }
12668
12962
  };
12669
12963
  };
@@ -12701,6 +12995,400 @@ function createNeonConnector(config, deps = {}) {
12701
12995
  };
12702
12996
  }
12703
12997
 
12998
+ // src/connectors/cloud-run/client.ts
12999
+ function cloudRunRequestLogName(projectId) {
13000
+ return `projects/${projectId}/logs/run.googleapis.com%2Frequests`;
13001
+ }
13002
+ function buildCloudRunEntriesFilter(projectId, sinceIso) {
13003
+ return [
13004
+ `logName = "${cloudRunRequestLogName(projectId)}"`,
13005
+ `resource.type = "${CLOUD_RUN_RESOURCE_TYPE}"`,
13006
+ 'httpRequest.requestMethod != ""',
13007
+ `timestamp >= "${sinceIso}"`
13008
+ ].join(" AND ");
13009
+ }
13010
+ var CLOUD_RUN_RESOURCE_TYPE = "cloud_run_revision";
13011
+ var DEFAULT_LOOKBACK_MS2 = 24 * 60 * 60 * 1e3;
13012
+ var ENTRIES_LIST_URL2 = "https://logging.googleapis.com/v2/entries:list";
13013
+ var PAGE_SIZE2 = 1e3;
13014
+ var MAX_PAGES2 = 20;
13015
+ async function fetchCloudRunRequestLogEntries(creds, sinceIso, apiUrl = ENTRIES_LIST_URL2) {
13016
+ const filter = buildCloudRunEntriesFilter(creds.projectId, sinceIso);
13017
+ const out = [];
13018
+ let pageToken;
13019
+ for (let page = 0; page < MAX_PAGES2; page++) {
13020
+ const body = {
13021
+ resourceNames: [`projects/${creds.projectId}`],
13022
+ filter,
13023
+ orderBy: "timestamp asc",
13024
+ pageSize: PAGE_SIZE2,
13025
+ ...pageToken ? { pageToken } : {}
13026
+ };
13027
+ const res = await junctionFetch(
13028
+ apiUrl,
13029
+ {
13030
+ method: "POST",
13031
+ headers: {
13032
+ ...bearerAuthHeader(creds.accessToken),
13033
+ "Content-Type": "application/json"
13034
+ },
13035
+ body: JSON.stringify(body)
13036
+ },
13037
+ // accountKey: the GCP project id — one customer's Cloud Logging quota is
13038
+ // scoped per GCP project (ADR-131's per-(provider, accountKey) rate-limit
13039
+ // bucket), the same key Firebase's connector uses.
13040
+ { provider: "cloud-run", accountKey: creds.projectId }
13041
+ );
13042
+ if (!res.ok) {
13043
+ throw new Error(`Cloud Logging entries.list failed: ${res.status} ${res.statusText}`);
13044
+ }
13045
+ const json = await res.json();
13046
+ if (Array.isArray(json.entries)) out.push(...json.entries);
13047
+ if (!json.nextPageToken) break;
13048
+ pageToken = json.nextPageToken;
13049
+ }
13050
+ return out;
13051
+ }
13052
+
13053
+ // src/connectors/cloud-run/types.ts
13054
+ function readCloudRunCredentials(raw) {
13055
+ const projectId = raw["projectId"];
13056
+ const accessToken = raw["accessToken"];
13057
+ if (typeof projectId !== "string" || projectId.length === 0) {
13058
+ throw new Error("cloud-run connector: credentials.projectId must be a non-empty string");
13059
+ }
13060
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
13061
+ throw new Error("cloud-run connector: credentials.accessToken must be a non-empty string");
13062
+ }
13063
+ return { projectId, accessToken };
13064
+ }
13065
+ var CLOUD_RUN_TARGET_KIND = "cloud_run_revision";
13066
+ var FIELD_SEP2 = "\0";
13067
+ function packCloudRunTargetName(identity) {
13068
+ return [identity.serviceName, identity.method, identity.path].join(FIELD_SEP2);
13069
+ }
13070
+ function parseCloudRunTargetName(targetName) {
13071
+ const firstSep = targetName.indexOf(FIELD_SEP2);
13072
+ if (firstSep === -1) return null;
13073
+ const serviceName = targetName.slice(0, firstSep);
13074
+ const rest = targetName.slice(firstSep + 1);
13075
+ const secondSep = rest.indexOf(FIELD_SEP2);
13076
+ if (secondSep === -1) return null;
13077
+ const method = rest.slice(0, secondSep);
13078
+ const path56 = rest.slice(secondSep + 1);
13079
+ if (!serviceName || !method || !path56) return null;
13080
+ return { serviceName, method, path: path56 };
13081
+ }
13082
+
13083
+ // src/connectors/cloud-run/map.ts
13084
+ var CLOUD_RUN_RESOURCE_TYPE2 = "cloud_run_revision";
13085
+ function pathFromRequestUrl2(requestUrl) {
13086
+ if (typeof requestUrl !== "string" || requestUrl.length === 0) return null;
13087
+ if (requestUrl.startsWith("/")) {
13088
+ const withoutQuery = requestUrl.split("?")[0];
13089
+ return withoutQuery && withoutQuery.length > 0 ? withoutQuery : "/";
13090
+ }
13091
+ try {
13092
+ const candidate = requestUrl.startsWith("//") ? `https:${requestUrl}` : requestUrl;
13093
+ const parsed = new URL(candidate);
13094
+ return parsed.pathname || "/";
13095
+ } catch {
13096
+ return null;
13097
+ }
13098
+ }
13099
+ var ERROR_STATUS_THRESHOLD4 = 500;
13100
+ function mapLogEntryToSignal2(entry) {
13101
+ if (!entry || typeof entry !== "object") return null;
13102
+ if (entry.resource?.type !== CLOUD_RUN_RESOURCE_TYPE2) return null;
13103
+ const serviceName = entry.resource?.labels?.["service_name"];
13104
+ if (typeof serviceName !== "string" || serviceName.length === 0) return null;
13105
+ const req = entry.httpRequest;
13106
+ if (!req) return null;
13107
+ if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
13108
+ const method = req.requestMethod.toUpperCase();
13109
+ const path56 = pathFromRequestUrl2(req.requestUrl);
13110
+ if (path56 === null) return null;
13111
+ const timestamp = entry.timestamp;
13112
+ if (typeof timestamp !== "string" || timestamp.length === 0) return null;
13113
+ const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD4;
13114
+ return {
13115
+ targetKind: CLOUD_RUN_TARGET_KIND,
13116
+ targetName: packCloudRunTargetName({ serviceName, method, path: path56 }),
13117
+ callCount: 1,
13118
+ errorCount: isError ? 1 : 0,
13119
+ lastObservedIso: timestamp
13120
+ };
13121
+ }
13122
+ function mapLogEntriesToSignals2(entries) {
13123
+ const out = [];
13124
+ for (const entry of entries) {
13125
+ const signal = mapLogEntryToSignal2(entry);
13126
+ if (signal) out.push(signal);
13127
+ }
13128
+ return out;
13129
+ }
13130
+
13131
+ // src/connectors/cloud-run/resolve.ts
13132
+ import { EdgeType as EdgeType29, NodeType as NodeType25, infraId as infraId18 } from "@neat.is/types";
13133
+ var CLOUD_RUN_SERVICE_INFRA_KIND = "cloud-run-service";
13134
+ function findMatchingRouteNode2(graph, serviceName, method, normalizedPath) {
13135
+ let found = null;
13136
+ graph.forEachNode((_id, attrs) => {
13137
+ if (found) return;
13138
+ const node = attrs;
13139
+ if (node.type !== NodeType25.RouteNode) return;
13140
+ const route = attrs;
13141
+ if (route.service !== serviceName || !route.pathTemplate) return;
13142
+ if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
13143
+ const routeMethod = route.method.toUpperCase();
13144
+ if (routeMethod !== "ALL" && routeMethod !== method) return;
13145
+ found = route.id;
13146
+ });
13147
+ return found;
13148
+ }
13149
+ function createCloudRunResolveTarget(graph, config) {
13150
+ return (signal) => {
13151
+ if (signal.targetKind !== CLOUD_RUN_TARGET_KIND) return null;
13152
+ const identity = parseCloudRunTargetName(signal.targetName);
13153
+ if (!identity) return null;
13154
+ const { serviceName: gcpServiceName, method, path: path56 } = identity;
13155
+ const mappedService = config.serviceMap?.[gcpServiceName];
13156
+ if (mappedService) {
13157
+ const routeNodeId = findMatchingRouteNode2(
13158
+ graph,
13159
+ mappedService,
13160
+ method,
13161
+ normalizePathTemplate(path56)
13162
+ );
13163
+ if (routeNodeId) {
13164
+ return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: EdgeType29.CALLS };
13165
+ }
13166
+ }
13167
+ return {
13168
+ targetNodeId: infraId18(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
13169
+ serviceName: mappedService ?? gcpServiceName,
13170
+ edgeType: EdgeType29.CALLS,
13171
+ ensureInfraNode: {
13172
+ kind: CLOUD_RUN_SERVICE_INFRA_KIND,
13173
+ name: gcpServiceName,
13174
+ provider: "cloud-run"
13175
+ }
13176
+ };
13177
+ };
13178
+ }
13179
+
13180
+ // src/connectors/cloud-run/index.ts
13181
+ var CloudRunConnector = class {
13182
+ constructor(config = {}) {
13183
+ this.config = config;
13184
+ }
13185
+ config;
13186
+ provider = "cloud-run";
13187
+ async poll(ctx) {
13188
+ const creds = readCloudRunCredentials(ctx.credentials);
13189
+ const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_LOOKBACK_MS2;
13190
+ const sinceIso = boundedSinceIso(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
13191
+ const entries = await fetchCloudRunRequestLogEntries(creds, sinceIso, this.config.apiUrl);
13192
+ return mapLogEntriesToSignals2(entries);
13193
+ }
13194
+ };
13195
+ function boundedSinceIso(since, now, maxLookbackMs) {
13196
+ const floor = new Date(now.getTime() - maxLookbackMs);
13197
+ if (!since) return floor.toISOString();
13198
+ const sinceMs = new Date(since).getTime();
13199
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
13200
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
13201
+ }
13202
+ function createCloudRunConnector(graph, config = {}) {
13203
+ return {
13204
+ connector: new CloudRunConnector(config),
13205
+ resolveTarget: createCloudRunResolveTarget(graph, config)
13206
+ };
13207
+ }
13208
+
13209
+ // src/connectors/render/index.ts
13210
+ import { EdgeType as EdgeType30, NodeType as NodeType26 } from "@neat.is/types";
13211
+
13212
+ // src/connectors/render/types.ts
13213
+ function readRenderToken(credentials) {
13214
+ const token = credentials.token;
13215
+ if (typeof token !== "string" || token.length === 0) {
13216
+ throw new Error("Render connector requires ctx.credentials.token (a Render API key)");
13217
+ }
13218
+ return token;
13219
+ }
13220
+ function renderLabelValue(entry, name) {
13221
+ if (!Array.isArray(entry.labels)) return void 0;
13222
+ const label = entry.labels.find((l) => l && typeof l === "object" && l.name === name);
13223
+ return label && typeof label.value === "string" ? label.value : void 0;
13224
+ }
13225
+
13226
+ // src/connectors/render/client.ts
13227
+ var DEFAULT_RENDER_API_URL = "https://api.render.com/v1";
13228
+ var DEFAULT_RENDER_LOG_LIMIT = 100;
13229
+ var RENDER_MAX_LOG_LIMIT = 100;
13230
+ var DEFAULT_RENDER_MAX_PAGES = 20;
13231
+ var DEFAULT_MAX_LOOKBACK_MS4 = 24 * 60 * 60 * 1e3;
13232
+ function clampLimit(limit) {
13233
+ const raw = Math.trunc(limit ?? DEFAULT_RENDER_LOG_LIMIT);
13234
+ if (!Number.isFinite(raw) || raw < 1) return DEFAULT_RENDER_LOG_LIMIT;
13235
+ return Math.min(raw, RENDER_MAX_LOG_LIMIT);
13236
+ }
13237
+ function boundedRenderStartTime(since, now, maxLookbackMs) {
13238
+ const floor = new Date(now.getTime() - maxLookbackMs);
13239
+ if (!since) return floor.toISOString();
13240
+ const sinceMs = new Date(since).getTime();
13241
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
13242
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
13243
+ }
13244
+ async function fetchRenderLogPage(config, token, startTime, endTime, limit, fetchImpl) {
13245
+ const url = new URL(`${config.apiUrl ?? DEFAULT_RENDER_API_URL}/logs`);
13246
+ url.searchParams.set("ownerId", config.ownerId);
13247
+ url.searchParams.set("resource", config.resourceId);
13248
+ url.searchParams.set("type", "request");
13249
+ url.searchParams.set("startTime", startTime);
13250
+ url.searchParams.set("endTime", endTime);
13251
+ url.searchParams.set("direction", "backward");
13252
+ url.searchParams.set("limit", String(limit));
13253
+ const res = await junctionFetch(
13254
+ url,
13255
+ { method: "GET", headers: { ...bearerAuthHeader(token) } },
13256
+ { provider: "render", accountKey: config.ownerId, ...fetchImpl ? { fetchImpl } : {} }
13257
+ );
13258
+ if (!res.ok) {
13259
+ throw new Error(`Render logs request failed: ${res.status} ${res.statusText}`);
13260
+ }
13261
+ return await res.json();
13262
+ }
13263
+ async function fetchRenderRequestLogs(config, token, startTime, endTime, fetchImpl) {
13264
+ const limit = clampLimit(config.limit);
13265
+ const maxPages = Math.max(1, Math.trunc(config.maxPages ?? DEFAULT_RENDER_MAX_PAGES));
13266
+ const out = [];
13267
+ let pageStart = startTime;
13268
+ let pageEnd = endTime;
13269
+ for (let page = 0; page < maxPages; page++) {
13270
+ const body = await fetchRenderLogPage(config, token, pageStart, pageEnd, limit, fetchImpl);
13271
+ if (Array.isArray(body.logs)) out.push(...body.logs);
13272
+ if (!body.hasMore || !body.nextStartTime || !body.nextEndTime) break;
13273
+ pageStart = body.nextStartTime;
13274
+ pageEnd = body.nextEndTime;
13275
+ }
13276
+ return out;
13277
+ }
13278
+
13279
+ // src/connectors/render/index.ts
13280
+ var ROUTE_TARGET_KIND2 = "route";
13281
+ var UNMATCHED_ROUTE_TARGET_KIND2 = "unmatched-route";
13282
+ function buildRenderRouteIndex(graph, serviceName) {
13283
+ const out = [];
13284
+ graph.forEachNode((_id, attrs) => {
13285
+ const node = attrs;
13286
+ if (node.type !== NodeType26.RouteNode) return;
13287
+ const route = attrs;
13288
+ if (route.service !== serviceName) return;
13289
+ out.push({
13290
+ method: route.method.toUpperCase(),
13291
+ normalizedPath: normalizePathTemplate(route.pathTemplate),
13292
+ routeNodeId: route.id,
13293
+ path: route.path,
13294
+ line: route.line
13295
+ });
13296
+ });
13297
+ return out;
13298
+ }
13299
+ function findRenderRoute(entries, method, normalizedPath) {
13300
+ return entries.find(
13301
+ (e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || e.method === method)
13302
+ );
13303
+ }
13304
+ function bucketKey3(method, normalizedPath) {
13305
+ return `${method} ${normalizedPath}`;
13306
+ }
13307
+ function isHttpErrorStatus2(status) {
13308
+ return status >= 400;
13309
+ }
13310
+ function upsertBucket2(buckets2, key, isError, timestamp, build) {
13311
+ const existing = buckets2.get(key);
13312
+ if (existing) {
13313
+ existing.callCount += 1;
13314
+ if (isError) existing.errorCount += 1;
13315
+ if (timestamp > existing.lastObservedIso) existing.lastObservedIso = timestamp;
13316
+ return;
13317
+ }
13318
+ buckets2.set(key, { callCount: 1, errorCount: isError ? 1 : 0, lastObservedIso: timestamp, ...build() });
13319
+ }
13320
+ function mapRenderRequestLogsToSignals(entries, routeIndex) {
13321
+ const buckets2 = /* @__PURE__ */ new Map();
13322
+ if (!Array.isArray(entries)) return [];
13323
+ for (const entry of entries) {
13324
+ if (!entry || typeof entry !== "object") continue;
13325
+ if (typeof entry.timestamp !== "string") continue;
13326
+ const method = renderLabelValue(entry, "method");
13327
+ const rawPath = renderLabelValue(entry, "path");
13328
+ if (typeof method !== "string" || method.length === 0) continue;
13329
+ if (typeof rawPath !== "string" || rawPath.length === 0) continue;
13330
+ const methodUpper = method.toUpperCase();
13331
+ const pathOnly = rawPath.split("?")[0];
13332
+ const normalizedPath = normalizePathTemplate(pathOnly);
13333
+ const statusCode = Number.parseInt(renderLabelValue(entry, "statusCode") ?? "", 10);
13334
+ const isError = Number.isFinite(statusCode) && isHttpErrorStatus2(statusCode);
13335
+ const match = findRenderRoute(routeIndex, methodUpper, normalizedPath);
13336
+ if (match) {
13337
+ upsertBucket2(buckets2, `route:${match.routeNodeId}`, isError, entry.timestamp, () => ({
13338
+ targetKind: ROUTE_TARGET_KIND2,
13339
+ targetName: match.routeNodeId,
13340
+ // RouteNode.line is optional in the schema (packages/types/src/
13341
+ // nodes.ts) even though routes.ts always sets it today — skip the
13342
+ // callSite rather than fabricate a line when it's ever absent
13343
+ // (file-awareness.md §6).
13344
+ ...match.line !== void 0 ? { callSite: { file: match.path, line: match.line } } : {}
13345
+ }));
13346
+ } else {
13347
+ upsertBucket2(
13348
+ buckets2,
13349
+ `unmatched:${bucketKey3(methodUpper, normalizedPath)}`,
13350
+ isError,
13351
+ entry.timestamp,
13352
+ () => ({
13353
+ targetKind: UNMATCHED_ROUTE_TARGET_KIND2,
13354
+ targetName: bucketKey3(methodUpper, normalizedPath)
13355
+ })
13356
+ );
13357
+ }
13358
+ }
13359
+ return [...buckets2.values()].map((b) => ({
13360
+ targetKind: b.targetKind,
13361
+ targetName: b.targetName,
13362
+ callCount: b.callCount,
13363
+ errorCount: b.errorCount,
13364
+ lastObservedIso: b.lastObservedIso,
13365
+ ...b.callSite ? { callSite: b.callSite } : {}
13366
+ }));
13367
+ }
13368
+ function createRenderResolveTarget(config) {
13369
+ return (signal) => {
13370
+ if (signal.targetKind === ROUTE_TARGET_KIND2) {
13371
+ return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: EdgeType30.CALLS };
13372
+ }
13373
+ return null;
13374
+ };
13375
+ }
13376
+ function createRenderConnector(graph, config) {
13377
+ return {
13378
+ provider: "render",
13379
+ async poll(ctx) {
13380
+ const token = readRenderToken(ctx.credentials);
13381
+ const now = /* @__PURE__ */ new Date();
13382
+ const maxLookbackMs = config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS4;
13383
+ const startTime = boundedRenderStartTime(ctx.since, now, maxLookbackMs);
13384
+ const endTime = now.toISOString();
13385
+ const logs = await fetchRenderRequestLogs(config, token, startTime, endTime);
13386
+ const routeIndex = buildRenderRouteIndex(graph, config.serviceName);
13387
+ return mapRenderRequestLogsToSignals(logs, routeIndex);
13388
+ }
13389
+ };
13390
+ }
13391
+
12704
13392
  // src/connectors/registry.ts
12705
13393
  var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
12706
13394
  async function authProbe(input) {
@@ -12876,6 +13564,71 @@ var PROVIDER_DISPATCH = {
12876
13564
  return { ok: false, reason: `neon telemetry read failed: ${err.message}` };
12877
13565
  }
12878
13566
  }
13567
+ },
13568
+ "cloud-run": {
13569
+ provider: "cloud-run",
13570
+ // Cloud Run reads both projectId and accessToken from the credential; the
13571
+ // single-string form maps to the secret (the token), and the required-fields
13572
+ // check below catches a projectId that was never supplied.
13573
+ primaryCredentialKey: "accessToken",
13574
+ requiredCredentialFields: ["projectId", "accessToken"],
13575
+ requiredOptionFields: [],
13576
+ build(graph, options) {
13577
+ return createCloudRunConnector(graph, options);
13578
+ },
13579
+ // POST entries:list with pageSize 1 — the exact surface poll() reads, so the
13580
+ // probe checks the actual `logging.logEntries.list` permission the connector
13581
+ // needs. A GET on the lighter logs.list endpoint (as Firebase probes) would
13582
+ // instead check `logging.logs.list`, falsely rejecting a correctly-scoped
13583
+ // custom role that carries only `logging.logEntries.list` (the narrowest
13584
+ // grant docs/connectors/cloud-run.md documents) — the same false-negative
13585
+ // trap Railway's validate avoids by probing its real query. A 2xx means the
13586
+ // token can list log entries; 401/403 means the provider rejected it.
13587
+ validate({ credentials, fetchImpl }) {
13588
+ const projectId = String(credentials.projectId ?? "");
13589
+ return authProbe({
13590
+ provider: "cloud-run",
13591
+ accountKey: projectId || "validate",
13592
+ url: "https://logging.googleapis.com/v2/entries:list",
13593
+ token: String(credentials.accessToken ?? ""),
13594
+ init: {
13595
+ method: "POST",
13596
+ headers: { "Content-Type": "application/json" },
13597
+ body: JSON.stringify({ resourceNames: [`projects/${projectId}`], pageSize: 1 })
13598
+ },
13599
+ ...fetchImpl ? { fetchImpl } : {}
13600
+ });
13601
+ }
13602
+ },
13603
+ render: {
13604
+ provider: "render",
13605
+ primaryCredentialKey: "token",
13606
+ requiredCredentialFields: ["token"],
13607
+ requiredOptionFields: ["ownerId", "resourceId", "serviceName"],
13608
+ build(graph, options) {
13609
+ const config = options;
13610
+ return {
13611
+ connector: createRenderConnector(graph, config),
13612
+ resolveTarget: createRenderResolveTarget(config)
13613
+ };
13614
+ },
13615
+ // GET /v1/services?limit=1 — the cheapest read the Render API key
13616
+ // authenticates against (render.com/docs/api). Unlike Railway's GraphQL
13617
+ // gateway, Render is a plain REST API: a live key returns 2xx, a bad one a
13618
+ // 401/403, so authProbe's status-code check is a true verdict here. The
13619
+ // logs query itself also needs an ownerId + resource; `services` needs
13620
+ // neither and still fails 401 on a bad token, so it's the honest probe.
13621
+ validate({ credentials, options, fetchImpl }) {
13622
+ const cfg = options;
13623
+ const baseUrl = cfg.apiUrl ?? DEFAULT_RENDER_API_URL;
13624
+ return authProbe({
13625
+ provider: "render",
13626
+ accountKey: cfg.ownerId ?? "validate",
13627
+ url: `${baseUrl}/services?limit=1`,
13628
+ token: String(credentials.token ?? ""),
13629
+ ...fetchImpl ? { fetchImpl } : {}
13630
+ });
13631
+ }
12879
13632
  }
12880
13633
  };
12881
13634
  function vercelCredsFrom(credentials) {
@@ -14058,4 +14811,4 @@ export {
14058
14811
  deprovisionConnector,
14059
14812
  buildApi
14060
14813
  };
14061
- //# sourceMappingURL=chunk-UU4XKSTO.js.map
14814
+ //# sourceMappingURL=chunk-KB4HRB6N.js.map