@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.
package/dist/neatd.cjs CHANGED
@@ -8333,6 +8333,55 @@ function columnsFromClassBody(body) {
8333
8333
  }
8334
8334
  return out;
8335
8335
  }
8336
+ function foreignKeyParentTable(call) {
8337
+ const fn = call.childForFieldName("function");
8338
+ const t = fn?.text;
8339
+ if (!t) return null;
8340
+ const base = t.includes(".") ? t.slice(t.lastIndexOf(".") + 1) : t;
8341
+ if (base !== "ForeignKey") return null;
8342
+ const target = firstPositionalString(call);
8343
+ if (!target) return null;
8344
+ const parts = target.split(".");
8345
+ if (parts.length < 2) return null;
8346
+ return parts[parts.length - 2];
8347
+ }
8348
+ function sqlalchemyForeignKeys(file, serviceDir) {
8349
+ if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
8350
+ const tree = parseSource6(makePyParser4(), file.content);
8351
+ const out = [];
8352
+ const seen = /* @__PURE__ */ new Set();
8353
+ walk3(tree.rootNode, (node) => {
8354
+ if (node.type !== "class_definition") return;
8355
+ const body = node.childForFieldName("body");
8356
+ const nameNode = node.childForFieldName("name");
8357
+ if (!body || !nameNode) return;
8358
+ const explicit = explicitTablename(body);
8359
+ if (explicit === "computed") return;
8360
+ let childTable = null;
8361
+ if (explicit) childTable = explicit.name;
8362
+ else if (extendsFlaskModel(node)) childTable = flaskSqlalchemyTableName(nameNode.text);
8363
+ if (!childTable) return;
8364
+ walk3(body, (n) => {
8365
+ if (n.type !== "call") return;
8366
+ const parentTable = foreignKeyParentTable(n);
8367
+ if (!parentTable) return;
8368
+ const key = `${childTable}->${parentTable}`;
8369
+ if (seen.has(key)) return;
8370
+ seen.add(key);
8371
+ const line = n.startPosition.row + 1;
8372
+ out.push({
8373
+ childTable,
8374
+ parentTable,
8375
+ evidence: {
8376
+ file: import_node_path35.default.relative(serviceDir, file.path),
8377
+ line,
8378
+ snippet: snippet(file.content, line)
8379
+ }
8380
+ });
8381
+ });
8382
+ });
8383
+ return out;
8384
+ }
8336
8385
  function sqlalchemyEndpointsFromFile(file, serviceDir) {
8337
8386
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
8338
8387
  const tree = parseSource6(makePyParser4(), file.content);
@@ -8678,6 +8727,92 @@ function drizzleEndpointsFromFile(file, serviceDir) {
8678
8727
  walk6(tree.rootNode);
8679
8728
  return out;
8680
8729
  }
8730
+ function enclosingVarName(call) {
8731
+ let node = call;
8732
+ while (node?.parent) {
8733
+ const parent = node.parent;
8734
+ if (parent.type === "variable_declarator") {
8735
+ const name = parent.childForFieldName("name");
8736
+ return name?.type === "identifier" ? name.text : null;
8737
+ }
8738
+ if (parent.type === "call_expression" || parent.type === "member_expression") {
8739
+ node = parent;
8740
+ continue;
8741
+ }
8742
+ return null;
8743
+ }
8744
+ return null;
8745
+ }
8746
+ function collectDrizzleTables(root) {
8747
+ const tables = [];
8748
+ const varToTable = /* @__PURE__ */ new Map();
8749
+ const walk6 = (node) => {
8750
+ if (node.type === "call_expression") {
8751
+ const fn = node.childForFieldName("function");
8752
+ if (fn?.type === "identifier" && TABLE_BUILDERS.has(fn.text)) {
8753
+ const args = node.childForFieldName("arguments");
8754
+ const argNodes = args ? namedChildren3(args) : [];
8755
+ const tableName = stringLiteralText2(argNodes[0] ?? null);
8756
+ const obj = argNodes[1]?.type === "object" ? argNodes[1] : null;
8757
+ if (tableName) {
8758
+ tables.push({ tableName, object: obj });
8759
+ const varName = enclosingVarName(node);
8760
+ if (varName) varToTable.set(varName, tableName);
8761
+ }
8762
+ }
8763
+ }
8764
+ for (const c of namedChildren3(node)) walk6(c);
8765
+ };
8766
+ walk6(root);
8767
+ return { tables, varToTable };
8768
+ }
8769
+ function referencesTargetVar(call) {
8770
+ const fn = call.childForFieldName("function");
8771
+ if (fn?.type !== "member_expression") return null;
8772
+ if (fn.childForFieldName("property")?.text !== "references") return null;
8773
+ const args = call.childForFieldName("arguments");
8774
+ const first = args ? namedChildren3(args)[0] ?? null : null;
8775
+ if (first?.type !== "arrow_function") return null;
8776
+ const body = first.childForFieldName("body");
8777
+ if (body?.type !== "member_expression") return null;
8778
+ const obj = body.childForFieldName("object");
8779
+ return obj?.type === "identifier" ? obj.text : null;
8780
+ }
8781
+ function drizzleForeignKeys(file, serviceDir) {
8782
+ if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
8783
+ const tree = parseSource3(parserForExt(import_node_path37.default.extname(file.path)), file.content);
8784
+ const { tables, varToTable } = collectDrizzleTables(tree.rootNode);
8785
+ const out = [];
8786
+ const seen = /* @__PURE__ */ new Set();
8787
+ for (const table of tables) {
8788
+ if (!table.object) continue;
8789
+ const walk6 = (node) => {
8790
+ if (node.type === "call_expression") {
8791
+ const targetVar = referencesTargetVar(node);
8792
+ const parentTable = targetVar ? varToTable.get(targetVar) : void 0;
8793
+ if (parentTable) {
8794
+ const key = `${table.tableName}->${parentTable}`;
8795
+ if (!seen.has(key)) {
8796
+ seen.add(key);
8797
+ const line = node.startPosition.row + 1;
8798
+ out.push({
8799
+ childTable: table.tableName,
8800
+ parentTable,
8801
+ evidence: {
8802
+ file: import_node_path37.default.relative(serviceDir, file.path),
8803
+ line,
8804
+ snippet: snippet(file.content, line)
8805
+ }
8806
+ });
8807
+ }
8808
+ }
8809
+ }
8810
+ for (const c of namedChildren3(node)) walk6(c);
8811
+ };
8812
+ walk6(table.object);
8813
+ }
8814
+ return out;
8815
+ }
8681
8816
 
8682
8817
  // src/extract/calls/prisma.ts
8683
8818
  init_cjs_shims();
@@ -8806,6 +8941,96 @@ async function prismaColumnEndpoints(serviceDir) {
8806
8941
  if (!content) return [];
8807
8942
  return prismaColumnsFromSchema({ path: schemaPath, content }, serviceDir);
8808
8943
  }
8944
+ function buildModelTableMap(lines) {
8945
+ const map = /* @__PURE__ */ new Map();
8946
+ let current = null;
8947
+ let depth = 0;
8948
+ for (const raw of lines) {
8949
+ if (current === null) {
8950
+ const header = raw.match(/^\s*model\s+([A-Za-z_]\w*)\b/);
8951
+ if (header && raw.includes("{")) {
8952
+ current = { model: header[1], table: header[1] };
8953
+ depth = netBraces(raw);
8954
+ if (depth <= 0) {
8955
+ map.set(current.model, current.table);
8956
+ current = null;
8957
+ }
8958
+ }
8959
+ continue;
8960
+ }
8961
+ depth += netBraces(raw);
8962
+ const trimmed = stripLineComment(raw).trim();
8963
+ if (trimmed.startsWith("@@")) {
8964
+ const m = trimmed.match(/@@map\(\s*"([^"]+)"\s*\)/);
8965
+ if (m) current.table = m[1];
8966
+ }
8967
+ if (depth <= 0) {
8968
+ map.set(current.model, current.table);
8969
+ current = null;
8970
+ }
8971
+ }
8972
+ if (current) map.set(current.model, current.table);
8973
+ return map;
8974
+ }
8975
+ function prismaForeignKeysFromSchema(file, serviceDir) {
8976
+ const content = file.content;
8977
+ if (!/\bmodel\s+[A-Za-z_]\w*\s*\{/.test(content)) return [];
8978
+ const lines = content.split("\n");
8979
+ const modelToTable = buildModelTableMap(lines);
8980
+ const out = [];
8981
+ const seen = /* @__PURE__ */ new Set();
8982
+ let current = null;
8983
+ let depth = 0;
8984
+ for (let i = 0; i < lines.length; i++) {
8985
+ const raw = lines[i];
8986
+ const lineNo = i + 1;
8987
+ if (current === null) {
8988
+ const header = raw.match(/^\s*model\s+([A-Za-z_]\w*)\b/);
8989
+ if (header && raw.includes("{")) {
8990
+ current = { table: modelToTable.get(header[1]) ?? header[1] };
8991
+ depth = netBraces(raw);
8992
+ if (depth <= 0) current = null;
8993
+ }
8994
+ continue;
8995
+ }
8996
+ depth += netBraces(raw);
8997
+ const closing = depth <= 0;
8998
+ const trimmed = stripLineComment(raw).trim();
8999
+ if (trimmed && !trimmed.startsWith("@@") && !trimmed.startsWith("}")) {
9000
+ const fm = trimmed.match(/^([A-Za-z_]\w*)\s+([A-Za-z_]\w*)/);
9001
+ if (fm && /@relation\b[^)]*\bfields\s*:/.test(trimmed)) {
9002
+ const parentTable = modelToTable.get(fm[2]);
9003
+ if (parentTable) {
9004
+ const key = `${current.table}->${parentTable}`;
9005
+ if (!seen.has(key)) {
9006
+ seen.add(key);
9007
+ out.push({
9008
+ childTable: current.table,
9009
+ parentTable,
9010
+ evidence: {
9011
+ file: import_node_path38.default.relative(serviceDir, file.path),
9012
+ line: lineNo,
9013
+ snippet: snippet(content, lineNo)
9014
+ }
9015
+ });
9016
+ }
9017
+ }
9018
+ }
9019
+ }
9020
+ if (closing) current = null;
9021
+ }
9022
+ return out;
9023
+ }
9024
+ async function prismaForeignKeys(serviceDir) {
9025
+ const schemaPath = await findFirst(serviceDir, [
9026
+ import_node_path38.default.join("prisma", "schema.prisma"),
9027
+ "schema.prisma"
9028
+ ]);
9029
+ if (!schemaPath) return [];
9030
+ const content = await readIfExists(schemaPath);
9031
+ if (!content) return [];
9032
+ return prismaForeignKeysFromSchema({ path: schemaPath, content }, serviceDir);
9033
+ }
8809
9034
 
8810
9035
  // src/extract/calls/go.ts
8811
9036
  init_cjs_shims();
@@ -8982,21 +9207,79 @@ async function addCallEdges(graph, services) {
8982
9207
  };
8983
9208
  }
8984
9209
 
9210
+ // src/extract/table-edges.ts
9211
+ init_cjs_shims();
9212
+ var import_types31 = require("@neat.is/types");
9213
+ async function addTableEdges(graph, services) {
9214
+ let nodesAdded = 0;
9215
+ let edgesAdded = 0;
9216
+ for (const service of services) {
9217
+ const files = await loadSourceFiles(service.dir);
9218
+ const refs = [];
9219
+ for (const file of files) {
9220
+ try {
9221
+ refs.push(...drizzleForeignKeys(file, service.dir));
9222
+ refs.push(...sqlalchemyForeignKeys(file, service.dir));
9223
+ } catch (err) {
9224
+ recordExtractionError("foreign-key extraction", file.path, err);
9225
+ }
9226
+ }
9227
+ try {
9228
+ refs.push(...await prismaForeignKeys(service.dir));
9229
+ } catch (err) {
9230
+ recordExtractionError("prisma foreign-key extraction", service.dir, err);
9231
+ }
9232
+ for (const ref of refs) {
9233
+ const childId = (0, import_types31.infraId)("sql-table", ref.childTable);
9234
+ const parentId = (0, import_types31.infraId)("sql-table", ref.parentTable);
9235
+ if (childId === parentId) continue;
9236
+ nodesAdded += ensureTableNode(graph, childId, ref.childTable);
9237
+ nodesAdded += ensureTableNode(graph, parentId, ref.parentTable);
9238
+ const edgeId = (0, import_types31.extractedEdgeId)(childId, parentId, import_types31.EdgeType.REFERENCES);
9239
+ if (graph.hasEdge(edgeId)) continue;
9240
+ const edge = {
9241
+ id: edgeId,
9242
+ source: childId,
9243
+ target: parentId,
9244
+ type: import_types31.EdgeType.REFERENCES,
9245
+ provenance: import_types31.Provenance.EXTRACTED,
9246
+ confidence: (0, import_types31.confidenceForExtracted)("structural"),
9247
+ evidence: ref.evidence
9248
+ };
9249
+ graph.addEdgeWithKey(edgeId, childId, parentId, edge);
9250
+ edgesAdded++;
9251
+ }
9252
+ }
9253
+ return { nodesAdded, edgesAdded };
9254
+ }
9255
+ function ensureTableNode(graph, id, name) {
9256
+ if (graph.hasNode(id)) return 0;
9257
+ const node = {
9258
+ id,
9259
+ type: import_types31.NodeType.InfraNode,
9260
+ name,
9261
+ provider: "self",
9262
+ kind: "sql-table"
9263
+ };
9264
+ graph.addNode(id, node);
9265
+ return 1;
9266
+ }
9267
+
8985
9268
  // src/extract/infra/index.ts
8986
9269
  init_cjs_shims();
8987
9270
 
8988
9271
  // src/extract/infra/docker-compose.ts
8989
9272
  init_cjs_shims();
8990
9273
  var import_node_path42 = __toESM(require("path"), 1);
8991
- var import_types32 = require("@neat.is/types");
9274
+ var import_types33 = require("@neat.is/types");
8992
9275
 
8993
9276
  // src/extract/infra/shared.ts
8994
9277
  init_cjs_shims();
8995
- var import_types31 = require("@neat.is/types");
9278
+ var import_types32 = require("@neat.is/types");
8996
9279
  function makeInfraNode(kind, name, provider = "self", extras) {
8997
9280
  return {
8998
- id: (0, import_types31.infraId)(kind, name),
8999
- type: import_types31.NodeType.InfraNode,
9281
+ id: (0, import_types32.infraId)(kind, name),
9282
+ type: import_types32.NodeType.InfraNode,
9000
9283
  name,
9001
9284
  provider,
9002
9285
  kind,
@@ -9040,8 +9323,8 @@ function emitPlatformResourceEdge(graph, anchorId, edgeType, kind, name, provide
9040
9323
  source: anchorId,
9041
9324
  target: node.id,
9042
9325
  type: edgeType,
9043
- provenance: import_types31.Provenance.EXTRACTED,
9044
- confidence: (0, import_types31.confidenceForExtracted)("structural"),
9326
+ provenance: import_types32.Provenance.EXTRACTED,
9327
+ confidence: (0, import_types32.confidenceForExtracted)("structural"),
9045
9328
  evidence: { file: evidenceFile, ...line !== void 0 ? { line } : {} }
9046
9329
  };
9047
9330
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -9108,15 +9391,15 @@ async function addComposeInfra(graph, scanPath, services) {
9108
9391
  for (const dep of dependsOnList(svc.depends_on)) {
9109
9392
  const targetId = composeNameToNodeId.get(dep);
9110
9393
  if (!targetId) continue;
9111
- const edgeId = (0, import_types3.extractedEdgeId)(sourceId, targetId, import_types32.EdgeType.DEPENDS_ON);
9394
+ const edgeId = (0, import_types3.extractedEdgeId)(sourceId, targetId, import_types33.EdgeType.DEPENDS_ON);
9112
9395
  if (graph.hasEdge(edgeId)) continue;
9113
9396
  const edge = {
9114
9397
  id: edgeId,
9115
9398
  source: sourceId,
9116
9399
  target: targetId,
9117
- type: import_types32.EdgeType.DEPENDS_ON,
9118
- provenance: import_types32.Provenance.EXTRACTED,
9119
- confidence: (0, import_types32.confidenceForExtracted)("structural"),
9400
+ type: import_types33.EdgeType.DEPENDS_ON,
9401
+ provenance: import_types33.Provenance.EXTRACTED,
9402
+ confidence: (0, import_types33.confidenceForExtracted)("structural"),
9120
9403
  evidence: { file: evidenceFile }
9121
9404
  };
9122
9405
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -9130,7 +9413,7 @@ async function addComposeInfra(graph, scanPath, services) {
9130
9413
  init_cjs_shims();
9131
9414
  var import_node_path43 = __toESM(require("path"), 1);
9132
9415
  var import_node_fs17 = require("fs");
9133
- var import_types33 = require("@neat.is/types");
9416
+ var import_types34 = require("@neat.is/types");
9134
9417
  function readDockerfile(content) {
9135
9418
  let image = null;
9136
9419
  const ports = [];
@@ -9189,15 +9472,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
9189
9472
  );
9190
9473
  nodesAdded += fn;
9191
9474
  edgesAdded += fe;
9192
- const edgeId = (0, import_types3.extractedEdgeId)(fileNodeId, node.id, import_types33.EdgeType.RUNS_ON);
9475
+ const edgeId = (0, import_types3.extractedEdgeId)(fileNodeId, node.id, import_types34.EdgeType.RUNS_ON);
9193
9476
  if (!graph.hasEdge(edgeId)) {
9194
9477
  const edge = {
9195
9478
  id: edgeId,
9196
9479
  source: fileNodeId,
9197
9480
  target: node.id,
9198
- type: import_types33.EdgeType.RUNS_ON,
9199
- provenance: import_types33.Provenance.EXTRACTED,
9200
- confidence: (0, import_types33.confidenceForExtracted)("structural"),
9481
+ type: import_types34.EdgeType.RUNS_ON,
9482
+ provenance: import_types34.Provenance.EXTRACTED,
9483
+ confidence: (0, import_types34.confidenceForExtracted)("structural"),
9201
9484
  evidence: {
9202
9485
  file: evidenceFile,
9203
9486
  ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
@@ -9212,15 +9495,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
9212
9495
  graph.addNode(portNode.id, portNode);
9213
9496
  nodesAdded++;
9214
9497
  }
9215
- const portEdgeId = (0, import_types3.extractedEdgeId)(fileNodeId, portNode.id, import_types33.EdgeType.CONNECTS_TO);
9498
+ const portEdgeId = (0, import_types3.extractedEdgeId)(fileNodeId, portNode.id, import_types34.EdgeType.CONNECTS_TO);
9216
9499
  if (graph.hasEdge(portEdgeId)) continue;
9217
9500
  const portEdge = {
9218
9501
  id: portEdgeId,
9219
9502
  source: fileNodeId,
9220
9503
  target: portNode.id,
9221
- type: import_types33.EdgeType.CONNECTS_TO,
9222
- provenance: import_types33.Provenance.EXTRACTED,
9223
- confidence: (0, import_types33.confidenceForExtracted)("structural"),
9504
+ type: import_types34.EdgeType.CONNECTS_TO,
9505
+ provenance: import_types34.Provenance.EXTRACTED,
9506
+ confidence: (0, import_types34.confidenceForExtracted)("structural"),
9224
9507
  evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
9225
9508
  };
9226
9509
  graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
@@ -9234,7 +9517,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
9234
9517
  init_cjs_shims();
9235
9518
  var import_node_fs18 = require("fs");
9236
9519
  var import_node_path44 = __toESM(require("path"), 1);
9237
- var import_types34 = require("@neat.is/types");
9520
+ var import_types35 = require("@neat.is/types");
9238
9521
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
9239
9522
  var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
9240
9523
  async function walkTfFiles(start, depth = 0, max = 5) {
@@ -9315,16 +9598,16 @@ async function addTerraformResources(graph, scanPath) {
9315
9598
  if (!target) continue;
9316
9599
  if (seen.has(target.nodeId)) continue;
9317
9600
  seen.add(target.nodeId);
9318
- const edgeId = (0, import_types3.extractedEdgeId)(resource.nodeId, target.nodeId, import_types34.EdgeType.DEPENDS_ON);
9601
+ const edgeId = (0, import_types3.extractedEdgeId)(resource.nodeId, target.nodeId, import_types35.EdgeType.DEPENDS_ON);
9319
9602
  if (graph.hasEdge(edgeId)) continue;
9320
9603
  const line = lineAt2(content, resource.bodyOffset + ref.index);
9321
9604
  const edge = {
9322
9605
  id: edgeId,
9323
9606
  source: resource.nodeId,
9324
9607
  target: target.nodeId,
9325
- type: import_types34.EdgeType.DEPENDS_ON,
9326
- provenance: import_types34.Provenance.EXTRACTED,
9327
- confidence: (0, import_types34.confidenceForExtracted)("structural"),
9608
+ type: import_types35.EdgeType.DEPENDS_ON,
9609
+ provenance: import_types35.Provenance.EXTRACTED,
9610
+ confidence: (0, import_types35.confidenceForExtracted)("structural"),
9328
9611
  evidence: { file: evidenceFile, line, snippet: key }
9329
9612
  };
9330
9613
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -9396,7 +9679,7 @@ init_cjs_shims();
9396
9679
  var import_node_fs20 = require("fs");
9397
9680
  var import_node_path46 = __toESM(require("path"), 1);
9398
9681
  var import_smol_toml2 = require("smol-toml");
9399
- var import_types35 = require("@neat.is/types");
9682
+ var import_types36 = require("@neat.is/types");
9400
9683
  var WRANGLER_FILENAMES = ["wrangler.toml", "wrangler.jsonc", "wrangler.json"];
9401
9684
  async function readWranglerConfig(dir) {
9402
9685
  for (const filename of WRANGLER_FILENAMES) {
@@ -9444,8 +9727,8 @@ function addResourceEdge(graph, anchorId, edgeType, kind, name, evidenceFile, li
9444
9727
  source: anchorId,
9445
9728
  target: node.id,
9446
9729
  type: edgeType,
9447
- provenance: import_types35.Provenance.EXTRACTED,
9448
- confidence: (0, import_types35.confidenceForExtracted)("structural"),
9730
+ provenance: import_types36.Provenance.EXTRACTED,
9731
+ confidence: (0, import_types36.confidenceForExtracted)("structural"),
9449
9732
  evidence: { file: evidenceFile, ...line !== void 0 ? { line } : {} }
9450
9733
  };
9451
9734
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -9506,15 +9789,15 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9506
9789
  nodesAdded++;
9507
9790
  }
9508
9791
  if (runtimeNode.id !== anchorId) {
9509
- const runsOnId = (0, import_types3.extractedEdgeId)(anchorId, runtimeNode.id, import_types35.EdgeType.RUNS_ON);
9792
+ const runsOnId = (0, import_types3.extractedEdgeId)(anchorId, runtimeNode.id, import_types36.EdgeType.RUNS_ON);
9510
9793
  if (!graph.hasEdge(runsOnId)) {
9511
9794
  const edge = {
9512
9795
  id: runsOnId,
9513
9796
  source: anchorId,
9514
9797
  target: runtimeNode.id,
9515
- type: import_types35.EdgeType.RUNS_ON,
9516
- provenance: import_types35.Provenance.EXTRACTED,
9517
- confidence: (0, import_types35.confidenceForExtracted)("structural"),
9798
+ type: import_types36.EdgeType.RUNS_ON,
9799
+ provenance: import_types36.Provenance.EXTRACTED,
9800
+ confidence: (0, import_types36.confidenceForExtracted)("structural"),
9518
9801
  evidence: {
9519
9802
  file: evidenceFile,
9520
9803
  ...config.compatibility_date ? { snippet: `compatibility_date = ${config.compatibility_date}`.slice(0, 120) } : {}
@@ -9528,7 +9811,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9528
9811
  const result = addResourceEdge(
9529
9812
  graph,
9530
9813
  anchorId,
9531
- import_types35.EdgeType.CONNECTS_TO,
9814
+ import_types36.EdgeType.CONNECTS_TO,
9532
9815
  "cloudflare-route",
9533
9816
  route,
9534
9817
  evidenceFile,
@@ -9552,7 +9835,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9552
9835
  const result = addResourceEdge(
9553
9836
  graph,
9554
9837
  anchorId,
9555
- import_types35.EdgeType.DEPENDS_ON,
9838
+ import_types36.EdgeType.DEPENDS_ON,
9556
9839
  group.kind,
9557
9840
  name,
9558
9841
  evidenceFile,
@@ -9566,7 +9849,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9566
9849
  const result = addResourceEdge(
9567
9850
  graph,
9568
9851
  anchorId,
9569
- import_types35.EdgeType.DEPENDS_ON,
9852
+ import_types36.EdgeType.DEPENDS_ON,
9570
9853
  "cloudflare-cron",
9571
9854
  cron,
9572
9855
  evidenceFile,
@@ -9579,7 +9862,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9579
9862
  const result = addResourceEdge(
9580
9863
  graph,
9581
9864
  anchorId,
9582
- import_types35.EdgeType.DEPENDS_ON,
9865
+ import_types36.EdgeType.DEPENDS_ON,
9583
9866
  "cloudflare-env-var",
9584
9867
  varName,
9585
9868
  evidenceFile,
@@ -9592,15 +9875,15 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9592
9875
  if (!svc.service) continue;
9593
9876
  const target = workerIndex.get(svc.service);
9594
9877
  if (target && target.anchorId !== anchorId) {
9595
- const edgeId = (0, import_types3.extractedEdgeId)(anchorId, target.anchorId, import_types35.EdgeType.CALLS);
9878
+ const edgeId = (0, import_types3.extractedEdgeId)(anchorId, target.anchorId, import_types36.EdgeType.CALLS);
9596
9879
  if (!graph.hasEdge(edgeId)) {
9597
9880
  const edge = {
9598
9881
  id: edgeId,
9599
9882
  source: anchorId,
9600
9883
  target: target.anchorId,
9601
- type: import_types35.EdgeType.CALLS,
9602
- provenance: import_types35.Provenance.EXTRACTED,
9603
- confidence: (0, import_types35.confidenceForExtracted)("structural"),
9884
+ type: import_types36.EdgeType.CALLS,
9885
+ provenance: import_types36.Provenance.EXTRACTED,
9886
+ confidence: (0, import_types36.confidenceForExtracted)("structural"),
9604
9887
  evidence: { file: evidenceFile, line: lineContaining2(raw, svc.service) }
9605
9888
  };
9606
9889
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -9611,7 +9894,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9611
9894
  const result = addResourceEdge(
9612
9895
  graph,
9613
9896
  anchorId,
9614
- import_types35.EdgeType.DEPENDS_ON,
9897
+ import_types36.EdgeType.DEPENDS_ON,
9615
9898
  "cloudflare-service-binding",
9616
9899
  svc.service,
9617
9900
  evidenceFile,
@@ -9628,7 +9911,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9628
9911
  init_cjs_shims();
9629
9912
  var import_node_fs21 = require("fs");
9630
9913
  var import_node_path47 = __toESM(require("path"), 1);
9631
- var import_types36 = require("@neat.is/types");
9914
+ var import_types37 = require("@neat.is/types");
9632
9915
  var VERCEL_CONFIG_FILENAMES = ["vercel.json", "vercel.jsonc"];
9633
9916
  async function readVercelConfig(dir) {
9634
9917
  for (const filename of VERCEL_CONFIG_FILENAMES) {
@@ -9691,12 +9974,12 @@ async function addVercelServices(graph, services, scanPath) {
9691
9974
  nodesAdded += result.nodesAdded;
9692
9975
  edgesAdded += result.edgesAdded;
9693
9976
  };
9694
- add(import_types36.EdgeType.RUNS_ON, "vercel", "vercel");
9695
- for (const cron of config.crons ?? []) add(import_types36.EdgeType.DEPENDS_ON, "vercel-cron", cron.path ?? cron.schedule);
9696
- for (const varName of Object.keys(config.env ?? {})) add(import_types36.EdgeType.DEPENDS_ON, "vercel-env-var", varName);
9697
- for (const varName of Object.keys(config.build?.env ?? {})) add(import_types36.EdgeType.DEPENDS_ON, "vercel-env-var", varName);
9977
+ add(import_types37.EdgeType.RUNS_ON, "vercel", "vercel");
9978
+ for (const cron of config.crons ?? []) add(import_types37.EdgeType.DEPENDS_ON, "vercel-cron", cron.path ?? cron.schedule);
9979
+ for (const varName of Object.keys(config.env ?? {})) add(import_types37.EdgeType.DEPENDS_ON, "vercel-env-var", varName);
9980
+ for (const varName of Object.keys(config.build?.env ?? {})) add(import_types37.EdgeType.DEPENDS_ON, "vercel-env-var", varName);
9698
9981
  for (const route of [...config.rewrites ?? [], ...config.redirects ?? [], ...config.routes ?? []]) {
9699
- add(import_types36.EdgeType.CONNECTS_TO, "vercel-route", routeSource(route));
9982
+ add(import_types37.EdgeType.CONNECTS_TO, "vercel-route", routeSource(route));
9700
9983
  }
9701
9984
  }
9702
9985
  return { nodesAdded, edgesAdded };
@@ -9707,7 +9990,7 @@ init_cjs_shims();
9707
9990
  var import_node_fs22 = require("fs");
9708
9991
  var import_node_path48 = __toESM(require("path"), 1);
9709
9992
  var import_smol_toml3 = require("smol-toml");
9710
- var import_types37 = require("@neat.is/types");
9993
+ var import_types38 = require("@neat.is/types");
9711
9994
  var RAILWAY_FILENAMES = ["railway.toml", "railway.json", "railway.jsonc"];
9712
9995
  async function readRailwayConfig(dir) {
9713
9996
  for (const filename of RAILWAY_FILENAMES) {
@@ -9753,9 +10036,9 @@ async function addRailwayServices(graph, services, scanPath) {
9753
10036
  nodesAdded += result.nodesAdded;
9754
10037
  edgesAdded += result.edgesAdded;
9755
10038
  };
9756
- add(import_types37.EdgeType.RUNS_ON, "railway", "railway");
9757
- add(import_types37.EdgeType.CONNECTS_TO, "railway-route", config.deploy?.healthcheckPath);
9758
- add(import_types37.EdgeType.DEPENDS_ON, "railway-cron", config.deploy?.cronSchedule);
10039
+ add(import_types38.EdgeType.RUNS_ON, "railway", "railway");
10040
+ add(import_types38.EdgeType.CONNECTS_TO, "railway-route", config.deploy?.healthcheckPath);
10041
+ add(import_types38.EdgeType.DEPENDS_ON, "railway-cron", config.deploy?.cronSchedule);
9759
10042
  }
9760
10043
  return { nodesAdded, edgesAdded };
9761
10044
  }
@@ -9765,7 +10048,7 @@ init_cjs_shims();
9765
10048
  var import_node_fs23 = require("fs");
9766
10049
  var import_node_path49 = __toESM(require("path"), 1);
9767
10050
  var import_smol_toml4 = require("smol-toml");
9768
- var import_types38 = require("@neat.is/types");
10051
+ var import_types39 = require("@neat.is/types");
9769
10052
  async function readSupabaseConfig(dir) {
9770
10053
  const relFile = import_node_path49.default.join("supabase", "config.toml");
9771
10054
  const abs = import_node_path49.default.join(dir, relFile);
@@ -9813,10 +10096,10 @@ async function addSupabaseProjects(graph, services, scanPath) {
9813
10096
  nodesAdded += result.nodesAdded;
9814
10097
  edgesAdded += result.edgesAdded;
9815
10098
  };
9816
- add(import_types38.EdgeType.RUNS_ON, "supabase", "supabase");
9817
- for (const fn of Object.keys(config.functions ?? {})) add(import_types38.EdgeType.DEPENDS_ON, "supabase-function", fn);
9818
- if (config.storage) add(import_types38.EdgeType.DEPENDS_ON, "supabase-storage", "storage");
9819
- if (config.auth) add(import_types38.EdgeType.DEPENDS_ON, "supabase-auth", "auth");
10099
+ add(import_types39.EdgeType.RUNS_ON, "supabase", "supabase");
10100
+ for (const fn of Object.keys(config.functions ?? {})) add(import_types39.EdgeType.DEPENDS_ON, "supabase-function", fn);
10101
+ if (config.storage) add(import_types39.EdgeType.DEPENDS_ON, "supabase-storage", "storage");
10102
+ if (config.auth) add(import_types39.EdgeType.DEPENDS_ON, "supabase-auth", "auth");
9820
10103
  }
9821
10104
  return { nodesAdded, edgesAdded };
9822
10105
  }
@@ -9844,11 +10127,11 @@ var import_node_path51 = __toESM(require("path"), 1);
9844
10127
  init_cjs_shims();
9845
10128
  var import_node_fs24 = require("fs");
9846
10129
  var import_node_path50 = __toESM(require("path"), 1);
9847
- var import_types39 = require("@neat.is/types");
10130
+ var import_types40 = require("@neat.is/types");
9848
10131
  function dropOrphanedFileNodes(graph) {
9849
10132
  const orphans = [];
9850
10133
  graph.forEachNode((id, attrs) => {
9851
- if (attrs.type !== import_types39.NodeType.FileNode) return;
10134
+ if (attrs.type !== import_types40.NodeType.FileNode) return;
9852
10135
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
9853
10136
  orphans.push(id);
9854
10137
  }
@@ -9861,7 +10144,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
9861
10144
  const bases = [scanPath, ...serviceDirs];
9862
10145
  graph.forEachEdge((id, attrs) => {
9863
10146
  const edge = attrs;
9864
- if (edge.provenance !== import_types39.Provenance.EXTRACTED) return;
10147
+ if (edge.provenance !== import_types40.Provenance.EXTRACTED) return;
9865
10148
  const evidenceFile = edge.evidence?.file;
9866
10149
  if (!evidenceFile) return;
9867
10150
  if (import_node_path50.default.isAbsolute(evidenceFile)) {
@@ -9892,6 +10175,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
9892
10175
  const routePhase = await addRoutes(graph, services);
9893
10176
  const grpcPhase = await addGrpcMethods(graph, services);
9894
10177
  const phase4 = await addCallEdges(graph, services);
10178
+ const tableEdges = await addTableEdges(graph, services);
9895
10179
  const phase5 = await addInfra(graph, scanPath, services);
9896
10180
  const ghostsRetired = retireExtractedEdgesByMissingFile(
9897
10181
  graph,
@@ -9931,8 +10215,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
9931
10215
  }
9932
10216
  }
9933
10217
  const result = {
9934
- nodesAdded: phase1Nodes + fileEnum.nodesAdded + symbolEnum.nodesAdded + importGraph.nodesAdded + symbolEdges.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
9935
- edgesAdded: fileEnum.edgesAdded + symbolEnum.edgesAdded + importGraph.edgesAdded + symbolEdges.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
10218
+ nodesAdded: phase1Nodes + fileEnum.nodesAdded + symbolEnum.nodesAdded + importGraph.nodesAdded + symbolEdges.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + tableEdges.nodesAdded + phase5.nodesAdded,
10219
+ edgesAdded: fileEnum.edgesAdded + symbolEnum.edgesAdded + importGraph.edgesAdded + symbolEdges.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + tableEdges.edgesAdded + phase5.edgesAdded,
9936
10220
  frontiersPromoted,
9937
10221
  extractionErrors: errorEntries.length,
9938
10222
  errorEntries,
@@ -9957,7 +10241,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
9957
10241
  init_cjs_shims();
9958
10242
  var import_node_fs25 = require("fs");
9959
10243
  var import_node_path52 = __toESM(require("path"), 1);
9960
- var import_types40 = require("@neat.is/types");
10244
+ var import_types41 = require("@neat.is/types");
9961
10245
  var SCHEMA_VERSION = 6;
9962
10246
  function migrateV1ToV2(payload) {
9963
10247
  const nodes = payload.graph.nodes;
@@ -9981,7 +10265,7 @@ function migrateV5ToV6(payload) {
9981
10265
  if (Array.isArray(nodes)) {
9982
10266
  for (const node of nodes) {
9983
10267
  const attrs = node.attributes;
9984
- if (!attrs || attrs.type !== import_types40.NodeType.InfraNode) continue;
10268
+ if (!attrs || attrs.type !== import_types41.NodeType.InfraNode) continue;
9985
10269
  if (attrs.kind !== "sql-table" && attrs.kind !== "supabase-table") continue;
9986
10270
  if (!Array.isArray(attrs.columns)) attrs.columns = [];
9987
10271
  }
@@ -9994,12 +10278,12 @@ function migrateV2ToV3(payload) {
9994
10278
  for (const edge of edges) {
9995
10279
  const attrs = edge.attributes;
9996
10280
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
9997
- attrs.provenance = import_types40.Provenance.OBSERVED;
10281
+ attrs.provenance = import_types41.Provenance.OBSERVED;
9998
10282
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
9999
10283
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
10000
10284
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
10001
10285
  if (type && source && target) {
10002
- const newId = (0, import_types40.observedEdgeId)(source, target, type);
10286
+ const newId = (0, import_types41.observedEdgeId)(source, target, type);
10003
10287
  attrs.id = newId;
10004
10288
  if (edge.key) edge.key = newId;
10005
10289
  }
@@ -10149,7 +10433,7 @@ var Projects = class {
10149
10433
  init_cjs_shims();
10150
10434
  var import_fastify2 = __toESM(require("fastify"), 1);
10151
10435
  var import_cors = __toESM(require("@fastify/cors"), 1);
10152
- var import_types59 = require("@neat.is/types");
10436
+ var import_types66 = require("@neat.is/types");
10153
10437
 
10154
10438
  // src/extend/index.ts
10155
10439
  init_cjs_shims();
@@ -10496,39 +10780,39 @@ async function rollbackExtension(ctx, args) {
10496
10780
 
10497
10781
  // src/divergences.ts
10498
10782
  init_cjs_shims();
10499
- var import_types41 = require("@neat.is/types");
10783
+ var import_types42 = require("@neat.is/types");
10500
10784
  function bucketKey(source, target, type) {
10501
10785
  return `${type}|${source}|${target}`;
10502
10786
  }
10503
10787
  function bucketSourceFor(graph, edge) {
10504
- if (edge.type !== import_types41.EdgeType.CONNECTS_TO) return edge.source;
10505
- const parsed = (0, import_types41.parseFileId)(edge.source);
10788
+ if (edge.type !== import_types42.EdgeType.CONNECTS_TO) return edge.source;
10789
+ const parsed = (0, import_types42.parseFileId)(edge.source);
10506
10790
  if (!parsed || !graph.hasNode(edge.target)) return edge.source;
10507
10791
  const target = graph.getNodeAttributes(edge.target);
10508
- if (target.type !== import_types41.NodeType.DatabaseNode) return edge.source;
10509
- return (0, import_types41.serviceId)(parsed.service);
10792
+ if (target.type !== import_types42.NodeType.DatabaseNode) return edge.source;
10793
+ return (0, import_types42.serviceId)(parsed.service);
10510
10794
  }
10511
10795
  function bucketEdges(graph) {
10512
10796
  const buckets2 = /* @__PURE__ */ new Map();
10513
10797
  graph.forEachEdge((id, attrs) => {
10514
10798
  const e = attrs;
10515
- const parsed = (0, import_types41.parseEdgeId)(id);
10799
+ const parsed = (0, import_types42.parseEdgeId)(id);
10516
10800
  const provenance = parsed?.provenance ?? e.provenance;
10517
10801
  const source = bucketSourceFor(graph, e);
10518
10802
  const key = bucketKey(source, e.target, e.type);
10519
10803
  const cur = buckets2.get(key) ?? { source, target: e.target, type: e.type };
10520
10804
  switch (provenance) {
10521
- case import_types41.Provenance.EXTRACTED:
10805
+ case import_types42.Provenance.EXTRACTED:
10522
10806
  cur.extracted = e;
10523
10807
  break;
10524
- case import_types41.Provenance.OBSERVED:
10808
+ case import_types42.Provenance.OBSERVED:
10525
10809
  cur.observed = e;
10526
10810
  break;
10527
- case import_types41.Provenance.INFERRED:
10811
+ case import_types42.Provenance.INFERRED:
10528
10812
  cur.inferred = e;
10529
10813
  break;
10530
10814
  default:
10531
- if (e.provenance === import_types41.Provenance.STALE) cur.stale = e;
10815
+ if (e.provenance === import_types42.Provenance.STALE) cur.stale = e;
10532
10816
  }
10533
10817
  buckets2.set(key, cur);
10534
10818
  });
@@ -10537,17 +10821,17 @@ function bucketEdges(graph) {
10537
10821
  function nodeIsFrontier(graph, nodeId) {
10538
10822
  if (!graph.hasNode(nodeId)) return false;
10539
10823
  const attrs = graph.getNodeAttributes(nodeId);
10540
- return attrs.type === import_types41.NodeType.FrontierNode;
10824
+ return attrs.type === import_types42.NodeType.FrontierNode;
10541
10825
  }
10542
10826
  function nodeIsWebsocketChannel(graph, nodeId) {
10543
10827
  if (!graph.hasNode(nodeId)) return false;
10544
10828
  const attrs = graph.getNodeAttributes(nodeId);
10545
- return attrs.type === import_types41.NodeType.WebSocketChannelNode;
10829
+ return attrs.type === import_types42.NodeType.WebSocketChannelNode;
10546
10830
  }
10547
10831
  function nodeIsSymbol(graph, nodeId) {
10548
10832
  if (!graph.hasNode(nodeId)) return false;
10549
10833
  const attrs = graph.getNodeAttributes(nodeId);
10550
- return attrs.type === import_types41.NodeType.SymbolNode;
10834
+ return attrs.type === import_types42.NodeType.SymbolNode;
10551
10835
  }
10552
10836
  function clampConfidence(n) {
10553
10837
  if (!Number.isFinite(n)) return 0;
@@ -10567,14 +10851,14 @@ function gradedConfidence(edge) {
10567
10851
  return clampConfidence(confidenceForEdge(edge));
10568
10852
  }
10569
10853
  var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
10570
- import_types41.EdgeType.CALLS,
10571
- import_types41.EdgeType.CONNECTS_TO,
10572
- import_types41.EdgeType.PUBLISHES_TO,
10573
- import_types41.EdgeType.CONSUMES_FROM
10854
+ import_types42.EdgeType.CALLS,
10855
+ import_types42.EdgeType.CONNECTS_TO,
10856
+ import_types42.EdgeType.PUBLISHES_TO,
10857
+ import_types42.EdgeType.CONSUMES_FROM
10574
10858
  ]);
10575
10859
  function detectMissingDivergences(graph, bucket) {
10576
10860
  const out = [];
10577
- if (bucket.type === import_types41.EdgeType.CONTAINS) return out;
10861
+ if (bucket.type === import_types42.EdgeType.CONTAINS) return out;
10578
10862
  if (nodeIsSymbol(graph, bucket.source) || nodeIsSymbol(graph, bucket.target)) return out;
10579
10863
  if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
10580
10864
  if (!nodeIsFrontier(graph, bucket.target)) {
@@ -10616,7 +10900,7 @@ function declaredHostFor(svc) {
10616
10900
  function hasExtractedConfiguredBy(graph, svcId) {
10617
10901
  for (const edgeId of graph.outboundEdges(svcId)) {
10618
10902
  const e = graph.getEdgeAttributes(edgeId);
10619
- if (e.type === import_types41.EdgeType.CONFIGURED_BY && e.provenance === import_types41.Provenance.EXTRACTED) {
10903
+ if (e.type === import_types42.EdgeType.CONFIGURED_BY && e.provenance === import_types42.Provenance.EXTRACTED) {
10620
10904
  return true;
10621
10905
  }
10622
10906
  }
@@ -10629,10 +10913,10 @@ function detectHostMismatch(graph, svcId, svc) {
10629
10913
  const out = [];
10630
10914
  for (const edgeId of graph.outboundEdges(svcId)) {
10631
10915
  const edge = graph.getEdgeAttributes(edgeId);
10632
- if (edge.type !== import_types41.EdgeType.CONNECTS_TO) continue;
10633
- if (edge.provenance !== import_types41.Provenance.OBSERVED) continue;
10916
+ if (edge.type !== import_types42.EdgeType.CONNECTS_TO) continue;
10917
+ if (edge.provenance !== import_types42.Provenance.OBSERVED) continue;
10634
10918
  const target = graph.getNodeAttributes(edge.target);
10635
- if (target.type !== import_types41.NodeType.DatabaseNode) continue;
10919
+ if (target.type !== import_types42.NodeType.DatabaseNode) continue;
10636
10920
  const observedHost = target.host?.trim();
10637
10921
  if (!observedHost) continue;
10638
10922
  if (observedHost === declaredHost) continue;
@@ -10654,10 +10938,10 @@ function detectCompatDivergences(graph, svcId, svc) {
10654
10938
  const deps = svc.dependencies ?? {};
10655
10939
  for (const edgeId of graph.outboundEdges(svcId)) {
10656
10940
  const edge = graph.getEdgeAttributes(edgeId);
10657
- if (edge.type !== import_types41.EdgeType.CONNECTS_TO) continue;
10658
- if (edge.provenance !== import_types41.Provenance.OBSERVED) continue;
10941
+ if (edge.type !== import_types42.EdgeType.CONNECTS_TO) continue;
10942
+ if (edge.provenance !== import_types42.Provenance.OBSERVED) continue;
10659
10943
  const target = graph.getNodeAttributes(edge.target);
10660
- if (target.type !== import_types41.NodeType.DatabaseNode) continue;
10944
+ if (target.type !== import_types42.NodeType.DatabaseNode) continue;
10661
10945
  for (const pair of compatPairs()) {
10662
10946
  if (pair.engine !== target.engine) continue;
10663
10947
  const declared = deps[pair.driver];
@@ -10754,7 +11038,7 @@ function suppressHostMismatchHalves(all) {
10754
11038
  for (const d of all) {
10755
11039
  if (d.type !== "host-mismatch") continue;
10756
11040
  observedHalf.add(`${d.source}->${d.target}`);
10757
- declaredHalf.add((0, import_types41.databaseId)(d.extractedHost));
11041
+ declaredHalf.add((0, import_types42.databaseId)(d.extractedHost));
10758
11042
  }
10759
11043
  if (observedHalf.size === 0) return all;
10760
11044
  return all.filter((d) => {
@@ -10773,13 +11057,13 @@ function computeDivergences(graph, opts = {}) {
10773
11057
  }
10774
11058
  graph.forEachNode((nodeId, attrs) => {
10775
11059
  const n = attrs;
10776
- if (n.type === import_types41.NodeType.ServiceNode) {
11060
+ if (n.type === import_types42.NodeType.ServiceNode) {
10777
11061
  const svc = n;
10778
11062
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
10779
11063
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
10780
11064
  return;
10781
11065
  }
10782
- if (n.type === import_types41.NodeType.InfraNode && n.kind === "sql-table") {
11066
+ if (n.type === import_types42.NodeType.InfraNode && n.kind === "sql-table") {
10783
11067
  for (const d of detectColumnDrift(n)) all.push(d);
10784
11068
  }
10785
11069
  });
@@ -10815,7 +11099,7 @@ function computeDivergences(graph, opts = {}) {
10815
11099
  const bc = "column" in b && b.column ? b.column : "";
10816
11100
  return ac.localeCompare(bc);
10817
11101
  });
10818
- return import_types41.DivergenceResultSchema.parse({
11102
+ return import_types42.DivergenceResultSchema.parse({
10819
11103
  divergences: filtered,
10820
11104
  totalAffected: filtered.length,
10821
11105
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -10949,7 +11233,7 @@ init_cjs_shims();
10949
11233
  var import_node_fs29 = require("fs");
10950
11234
  var import_node_os3 = __toESM(require("os"), 1);
10951
11235
  var import_node_path56 = __toESM(require("path"), 1);
10952
- var import_types42 = require("@neat.is/types");
11236
+ var import_types43 = require("@neat.is/types");
10953
11237
  var LOCK_TIMEOUT_MS = 5e3;
10954
11238
  var LOCK_RETRY_MS = 50;
10955
11239
  function neatHome() {
@@ -11151,10 +11435,10 @@ async function readRegistry() {
11151
11435
  throw err;
11152
11436
  }
11153
11437
  const parsed = JSON.parse(raw);
11154
- return import_types42.RegistryFileSchema.parse(parsed);
11438
+ return import_types43.RegistryFileSchema.parse(parsed);
11155
11439
  }
11156
11440
  async function writeRegistry(reg) {
11157
- const validated = import_types42.RegistryFileSchema.parse(reg);
11441
+ const validated = import_types43.RegistryFileSchema.parse(reg);
11158
11442
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
11159
11443
  }
11160
11444
  async function getProject(name) {
@@ -11505,15 +11789,15 @@ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_
11505
11789
 
11506
11790
  // src/connectors/index.ts
11507
11791
  init_cjs_shims();
11508
- var import_types43 = require("@neat.is/types");
11792
+ var import_types44 = require("@neat.is/types");
11509
11793
  var NO_ENV = "unknown";
11510
11794
  function staticCallSiteFor(graph, serviceName, targetNodeId) {
11511
11795
  if (!graph.hasNode(targetNodeId)) return void 0;
11512
11796
  const sites = [];
11513
11797
  for (const edgeId of graph.inboundEdges(targetNodeId)) {
11514
11798
  const edge = graph.getEdgeAttributes(edgeId);
11515
- if (edge.provenance !== import_types43.Provenance.EXTRACTED) continue;
11516
- const parsed = (0, import_types43.parseFileId)(edge.source);
11799
+ if (edge.provenance !== import_types44.Provenance.EXTRACTED) continue;
11800
+ const parsed = (0, import_types44.parseFileId)(edge.source);
11517
11801
  if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
11518
11802
  const site = { relPath: edge.evidence.file };
11519
11803
  if (edge.evidence.line !== void 0) site.line = edge.evidence.line;
@@ -11524,7 +11808,7 @@ function staticCallSiteFor(graph, serviceName, targetNodeId) {
11524
11808
  function routeCallSiteFor(graph, targetNodeId) {
11525
11809
  if (!graph.hasNode(targetNodeId)) return void 0;
11526
11810
  const attrs = graph.getNodeAttributes(targetNodeId);
11527
- if (attrs.type !== import_types43.NodeType.RouteNode || !attrs.path) return void 0;
11811
+ if (attrs.type !== import_types44.NodeType.RouteNode || !attrs.path) return void 0;
11528
11812
  const site = { relPath: attrs.path };
11529
11813
  if (attrs.line !== void 0) site.line = attrs.line;
11530
11814
  return site;
@@ -11708,7 +11992,11 @@ var JUNCTION_DEFAULT_RATE_LIMITS = {
11708
11992
  // connector add/remove/test` (provision/deprovision/validate), never a poll
11709
11993
  // loop, so this bucket is exercised a handful of times per command. Kept
11710
11994
  // conservative pending a documented Drains-API rate limit.
11711
- vercel: { capacity: 20, refillMs: 5e3 }
11995
+ vercel: { capacity: 20, refillMs: 5e3 },
11996
+ // Render's REST API (api-docs.render.com/reference/rate-limiting) isn't
11997
+ // pinned here to a single confirmed number — this is a conservative
11998
+ // placeholder pending a live project, matching the other pull providers.
11999
+ render: { capacity: 30, refillMs: 1e4 }
11712
12000
  };
11713
12001
  var JUNCTION_GENERIC_RATE_LIMIT = { capacity: 20, refillMs: 5e3 };
11714
12002
  function defaultRateLimitFor(provider) {
@@ -12115,23 +12403,23 @@ async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT
12115
12403
 
12116
12404
  // src/connectors/supabase/resolve.ts
12117
12405
  init_cjs_shims();
12118
- var import_types45 = require("@neat.is/types");
12406
+ var import_types46 = require("@neat.is/types");
12119
12407
  function createSupabaseResolveTarget(graph, config) {
12120
12408
  return (signal, _ctx) => {
12121
12409
  if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
12122
12410
  return null;
12123
12411
  }
12124
- const subResourceId = (0, import_types45.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
12412
+ const subResourceId = (0, import_types46.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
12125
12413
  if (graph.hasNode(subResourceId)) {
12126
- return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types45.EdgeType.CALLS };
12414
+ return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types46.EdgeType.CALLS };
12127
12415
  }
12128
- const bareResourceId = (0, import_types45.infraId)(signal.targetKind, signal.targetName);
12416
+ const bareResourceId = (0, import_types46.infraId)(signal.targetKind, signal.targetName);
12129
12417
  if (graph.hasNode(bareResourceId)) {
12130
- return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: import_types45.EdgeType.CALLS };
12418
+ return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: import_types46.EdgeType.CALLS };
12131
12419
  }
12132
- const projectLevelId = (0, import_types45.infraId)("supabase", config.nodeRef);
12420
+ const projectLevelId = (0, import_types46.infraId)("supabase", config.nodeRef);
12133
12421
  if (graph.hasNode(projectLevelId)) {
12134
- return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types45.EdgeType.CALLS };
12422
+ return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types46.EdgeType.CALLS };
12135
12423
  }
12136
12424
  return null;
12137
12425
  };
@@ -12224,7 +12512,7 @@ function createSupabaseConnector(graph, config, deps = {}) {
12224
12512
 
12225
12513
  // src/connectors/railway/index.ts
12226
12514
  init_cjs_shims();
12227
- var import_types49 = require("@neat.is/types");
12515
+ var import_types50 = require("@neat.is/types");
12228
12516
 
12229
12517
  // src/connectors/railway/client.ts
12230
12518
  init_cjs_shims();
@@ -12375,7 +12663,7 @@ function buildRailwayRouteIndex(graph, serviceName) {
12375
12663
  const out = [];
12376
12664
  graph.forEachNode((_id, attrs) => {
12377
12665
  const node = attrs;
12378
- if (node.type !== import_types49.NodeType.RouteNode) return;
12666
+ if (node.type !== import_types50.NodeType.RouteNode) return;
12379
12667
  const route = attrs;
12380
12668
  if (route.service !== serviceName) return;
12381
12669
  out.push({
@@ -12479,12 +12767,12 @@ function createRailwayResolveTarget(config) {
12479
12767
  const serviceName = config.serviceNameById[config.serviceId];
12480
12768
  if (!serviceName) return null;
12481
12769
  if (signal.targetKind === ROUTE_TARGET_KIND) {
12482
- return { targetNodeId: signal.targetName, serviceName, edgeType: import_types49.EdgeType.CALLS };
12770
+ return { targetNodeId: signal.targetName, serviceName, edgeType: import_types50.EdgeType.CALLS };
12483
12771
  }
12484
12772
  if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
12485
12773
  const peerName = config.serviceNameById[signal.targetName];
12486
12774
  if (!peerName) return null;
12487
- return { targetNodeId: (0, import_types49.serviceId)(peerName), serviceName, edgeType: import_types49.EdgeType.CONNECTS_TO };
12775
+ return { targetNodeId: (0, import_types50.serviceId)(peerName), serviceName, edgeType: import_types50.EdgeType.CONNECTS_TO };
12488
12776
  }
12489
12777
  return null;
12490
12778
  };
@@ -12672,7 +12960,7 @@ function mapLogEntriesToSignals(entries) {
12672
12960
 
12673
12961
  // src/connectors/firebase/resolve.ts
12674
12962
  init_cjs_shims();
12675
- var import_types50 = require("@neat.is/types");
12963
+ var import_types51 = require("@neat.is/types");
12676
12964
  function neatServiceNameFor(resourceType, resourceName, serviceMap) {
12677
12965
  switch (resourceType) {
12678
12966
  case "cloud_function":
@@ -12687,7 +12975,7 @@ function routeEntriesFor(graph, serviceName) {
12687
12975
  const entries = [];
12688
12976
  graph.forEachNode((_id, attrs) => {
12689
12977
  const node = attrs;
12690
- if (node.type !== import_types50.NodeType.RouteNode) return;
12978
+ if (node.type !== import_types51.NodeType.RouteNode) return;
12691
12979
  const route = attrs;
12692
12980
  if (route.service !== serviceName) return;
12693
12981
  entries.push({
@@ -12719,7 +13007,7 @@ function createFirebaseResolveTarget(graph, serviceMap) {
12719
13007
  return {
12720
13008
  targetNodeId: match.routeNodeId,
12721
13009
  serviceName,
12722
- edgeType: import_types50.EdgeType.CALLS
13010
+ edgeType: import_types51.EdgeType.CALLS
12723
13011
  };
12724
13012
  };
12725
13013
  }
@@ -12746,7 +13034,7 @@ init_cjs_shims();
12746
13034
 
12747
13035
  // src/connectors/cloudflare/connector.ts
12748
13036
  init_cjs_shims();
12749
- var import_types52 = require("@neat.is/types");
13037
+ var import_types53 = require("@neat.is/types");
12750
13038
 
12751
13039
  // src/connectors/cloudflare/client.ts
12752
13040
  init_cjs_shims();
@@ -12910,7 +13198,7 @@ function findTaggedWorkerFileNode(graph, workerName) {
12910
13198
  graph.forEachNode((id, attrs) => {
12911
13199
  if (found) return;
12912
13200
  const a = attrs;
12913
- if (a.type === import_types52.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
13201
+ if (a.type === import_types53.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
12914
13202
  found = id;
12915
13203
  }
12916
13204
  });
@@ -12922,7 +13210,7 @@ function findMatchingRouteNode(graph, serviceName, method, path62) {
12922
13210
  graph.forEachNode((id, attrs) => {
12923
13211
  if (found) return;
12924
13212
  const a = attrs;
12925
- if (a.type !== import_types52.NodeType.RouteNode || a.service !== serviceName) return;
13213
+ if (a.type !== import_types53.NodeType.RouteNode || a.service !== serviceName) return;
12926
13214
  if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
12927
13215
  const routeMethod = (a.method ?? "").toUpperCase();
12928
13216
  if (routeMethod !== "ALL" && routeMethod !== method) return;
@@ -12941,11 +13229,11 @@ function createCloudflareResolveTarget(config, graph) {
12941
13229
  };
12942
13230
  const mapping = config.workers?.[scriptName];
12943
13231
  if (mapping) {
12944
- const wholeFileId = (0, import_types52.fileId)(mapping.service, mapping.entryFile);
13232
+ const wholeFileId = (0, import_types53.fileId)(mapping.service, mapping.entryFile);
12945
13233
  return {
12946
13234
  targetNodeId: resolveRouteGrain(mapping.service, wholeFileId),
12947
13235
  serviceName: mapping.service,
12948
- edgeType: import_types52.EdgeType.CALLS
13236
+ edgeType: import_types53.EdgeType.CALLS
12949
13237
  };
12950
13238
  }
12951
13239
  const taggedFileId = findTaggedWorkerFileNode(graph, scriptName);
@@ -12954,13 +13242,13 @@ function createCloudflareResolveTarget(config, graph) {
12954
13242
  return {
12955
13243
  targetNodeId: resolveRouteGrain(fileNode.service, taggedFileId),
12956
13244
  serviceName: fileNode.service,
12957
- edgeType: import_types52.EdgeType.CALLS
13245
+ edgeType: import_types53.EdgeType.CALLS
12958
13246
  };
12959
13247
  }
12960
13248
  return {
12961
- targetNodeId: (0, import_types52.infraId)("cloudflare-worker", scriptName),
13249
+ targetNodeId: (0, import_types53.infraId)("cloudflare-worker", scriptName),
12962
13250
  serviceName: scriptName,
12963
- edgeType: import_types52.EdgeType.CALLS,
13251
+ edgeType: import_types53.EdgeType.CALLS,
12964
13252
  ensureInfraNode: { kind: "cloudflare-worker", name: scriptName, provider: "cloudflare" }
12965
13253
  };
12966
13254
  };
@@ -13156,14 +13444,14 @@ function diffNeonStatementsToSignals(rows, previous, observedAtIso) {
13156
13444
 
13157
13445
  // src/connectors/neon/resolve.ts
13158
13446
  init_cjs_shims();
13159
- var import_types56 = require("@neat.is/types");
13447
+ var import_types57 = require("@neat.is/types");
13160
13448
  function createNeonResolveTarget(config) {
13161
13449
  return (signal) => {
13162
13450
  if (signal.targetKind !== NEON_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
13163
13451
  return {
13164
- targetNodeId: (0, import_types56.infraId)("sql-table", signal.targetName),
13452
+ targetNodeId: (0, import_types57.infraId)("sql-table", signal.targetName),
13165
13453
  serviceName: config.serviceName,
13166
- edgeType: import_types56.EdgeType.CALLS,
13454
+ edgeType: import_types57.EdgeType.CALLS,
13167
13455
  ensureInfraNode: { kind: "sql-table", name: signal.targetName, provider: "neon" }
13168
13456
  };
13169
13457
  };
@@ -13201,6 +13489,412 @@ function createNeonConnector(config, deps = {}) {
13201
13489
  };
13202
13490
  }
13203
13491
 
13492
+ // src/connectors/cloud-run/index.ts
13493
+ init_cjs_shims();
13494
+
13495
+ // src/connectors/cloud-run/client.ts
13496
+ init_cjs_shims();
13497
+ function cloudRunRequestLogName(projectId) {
13498
+ return `projects/${projectId}/logs/run.googleapis.com%2Frequests`;
13499
+ }
13500
+ function buildCloudRunEntriesFilter(projectId, sinceIso) {
13501
+ return [
13502
+ `logName = "${cloudRunRequestLogName(projectId)}"`,
13503
+ `resource.type = "${CLOUD_RUN_RESOURCE_TYPE}"`,
13504
+ 'httpRequest.requestMethod != ""',
13505
+ `timestamp >= "${sinceIso}"`
13506
+ ].join(" AND ");
13507
+ }
13508
+ var CLOUD_RUN_RESOURCE_TYPE = "cloud_run_revision";
13509
+ var DEFAULT_LOOKBACK_MS2 = 24 * 60 * 60 * 1e3;
13510
+ var ENTRIES_LIST_URL2 = "https://logging.googleapis.com/v2/entries:list";
13511
+ var PAGE_SIZE2 = 1e3;
13512
+ var MAX_PAGES2 = 20;
13513
+ async function fetchCloudRunRequestLogEntries(creds, sinceIso, apiUrl = ENTRIES_LIST_URL2) {
13514
+ const filter = buildCloudRunEntriesFilter(creds.projectId, sinceIso);
13515
+ const out = [];
13516
+ let pageToken;
13517
+ for (let page = 0; page < MAX_PAGES2; page++) {
13518
+ const body = {
13519
+ resourceNames: [`projects/${creds.projectId}`],
13520
+ filter,
13521
+ orderBy: "timestamp asc",
13522
+ pageSize: PAGE_SIZE2,
13523
+ ...pageToken ? { pageToken } : {}
13524
+ };
13525
+ const res = await junctionFetch(
13526
+ apiUrl,
13527
+ {
13528
+ method: "POST",
13529
+ headers: {
13530
+ ...bearerAuthHeader(creds.accessToken),
13531
+ "Content-Type": "application/json"
13532
+ },
13533
+ body: JSON.stringify(body)
13534
+ },
13535
+ // accountKey: the GCP project id — one customer's Cloud Logging quota is
13536
+ // scoped per GCP project (ADR-131's per-(provider, accountKey) rate-limit
13537
+ // bucket), the same key Firebase's connector uses.
13538
+ { provider: "cloud-run", accountKey: creds.projectId }
13539
+ );
13540
+ if (!res.ok) {
13541
+ throw new Error(`Cloud Logging entries.list failed: ${res.status} ${res.statusText}`);
13542
+ }
13543
+ const json = await res.json();
13544
+ if (Array.isArray(json.entries)) out.push(...json.entries);
13545
+ if (!json.nextPageToken) break;
13546
+ pageToken = json.nextPageToken;
13547
+ }
13548
+ return out;
13549
+ }
13550
+
13551
+ // src/connectors/cloud-run/map.ts
13552
+ init_cjs_shims();
13553
+
13554
+ // src/connectors/cloud-run/types.ts
13555
+ init_cjs_shims();
13556
+ function readCloudRunCredentials(raw) {
13557
+ const projectId = raw["projectId"];
13558
+ const accessToken = raw["accessToken"];
13559
+ if (typeof projectId !== "string" || projectId.length === 0) {
13560
+ throw new Error("cloud-run connector: credentials.projectId must be a non-empty string");
13561
+ }
13562
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
13563
+ throw new Error("cloud-run connector: credentials.accessToken must be a non-empty string");
13564
+ }
13565
+ return { projectId, accessToken };
13566
+ }
13567
+ var CLOUD_RUN_TARGET_KIND = "cloud_run_revision";
13568
+ var FIELD_SEP2 = "\0";
13569
+ function packCloudRunTargetName(identity) {
13570
+ return [identity.serviceName, identity.method, identity.path].join(FIELD_SEP2);
13571
+ }
13572
+ function parseCloudRunTargetName(targetName) {
13573
+ const firstSep = targetName.indexOf(FIELD_SEP2);
13574
+ if (firstSep === -1) return null;
13575
+ const serviceName = targetName.slice(0, firstSep);
13576
+ const rest = targetName.slice(firstSep + 1);
13577
+ const secondSep = rest.indexOf(FIELD_SEP2);
13578
+ if (secondSep === -1) return null;
13579
+ const method = rest.slice(0, secondSep);
13580
+ const path62 = rest.slice(secondSep + 1);
13581
+ if (!serviceName || !method || !path62) return null;
13582
+ return { serviceName, method, path: path62 };
13583
+ }
13584
+
13585
+ // src/connectors/cloud-run/map.ts
13586
+ var CLOUD_RUN_RESOURCE_TYPE2 = "cloud_run_revision";
13587
+ function pathFromRequestUrl2(requestUrl) {
13588
+ if (typeof requestUrl !== "string" || requestUrl.length === 0) return null;
13589
+ if (requestUrl.startsWith("/")) {
13590
+ const withoutQuery = requestUrl.split("?")[0];
13591
+ return withoutQuery && withoutQuery.length > 0 ? withoutQuery : "/";
13592
+ }
13593
+ try {
13594
+ const candidate = requestUrl.startsWith("//") ? `https:${requestUrl}` : requestUrl;
13595
+ const parsed = new URL(candidate);
13596
+ return parsed.pathname || "/";
13597
+ } catch {
13598
+ return null;
13599
+ }
13600
+ }
13601
+ var ERROR_STATUS_THRESHOLD4 = 500;
13602
+ function mapLogEntryToSignal2(entry2) {
13603
+ if (!entry2 || typeof entry2 !== "object") return null;
13604
+ if (entry2.resource?.type !== CLOUD_RUN_RESOURCE_TYPE2) return null;
13605
+ const serviceName = entry2.resource?.labels?.["service_name"];
13606
+ if (typeof serviceName !== "string" || serviceName.length === 0) return null;
13607
+ const req2 = entry2.httpRequest;
13608
+ if (!req2) return null;
13609
+ if (typeof req2.requestMethod !== "string" || req2.requestMethod.length === 0) return null;
13610
+ const method = req2.requestMethod.toUpperCase();
13611
+ const path62 = pathFromRequestUrl2(req2.requestUrl);
13612
+ if (path62 === null) return null;
13613
+ const timestamp = entry2.timestamp;
13614
+ if (typeof timestamp !== "string" || timestamp.length === 0) return null;
13615
+ const isError = typeof req2.status === "number" && req2.status >= ERROR_STATUS_THRESHOLD4;
13616
+ return {
13617
+ targetKind: CLOUD_RUN_TARGET_KIND,
13618
+ targetName: packCloudRunTargetName({ serviceName, method, path: path62 }),
13619
+ callCount: 1,
13620
+ errorCount: isError ? 1 : 0,
13621
+ lastObservedIso: timestamp
13622
+ };
13623
+ }
13624
+ function mapLogEntriesToSignals2(entries) {
13625
+ const out = [];
13626
+ for (const entry2 of entries) {
13627
+ const signal = mapLogEntryToSignal2(entry2);
13628
+ if (signal) out.push(signal);
13629
+ }
13630
+ return out;
13631
+ }
13632
+
13633
+ // src/connectors/cloud-run/resolve.ts
13634
+ init_cjs_shims();
13635
+ var import_types61 = require("@neat.is/types");
13636
+ var CLOUD_RUN_SERVICE_INFRA_KIND = "cloud-run-service";
13637
+ function findMatchingRouteNode2(graph, serviceName, method, normalizedPath) {
13638
+ let found = null;
13639
+ graph.forEachNode((_id, attrs) => {
13640
+ if (found) return;
13641
+ const node = attrs;
13642
+ if (node.type !== import_types61.NodeType.RouteNode) return;
13643
+ const route = attrs;
13644
+ if (route.service !== serviceName || !route.pathTemplate) return;
13645
+ if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
13646
+ const routeMethod = route.method.toUpperCase();
13647
+ if (routeMethod !== "ALL" && routeMethod !== method) return;
13648
+ found = route.id;
13649
+ });
13650
+ return found;
13651
+ }
13652
+ function createCloudRunResolveTarget(graph, config) {
13653
+ return (signal) => {
13654
+ if (signal.targetKind !== CLOUD_RUN_TARGET_KIND) return null;
13655
+ const identity = parseCloudRunTargetName(signal.targetName);
13656
+ if (!identity) return null;
13657
+ const { serviceName: gcpServiceName, method, path: path62 } = identity;
13658
+ const mappedService = config.serviceMap?.[gcpServiceName];
13659
+ if (mappedService) {
13660
+ const routeNodeId = findMatchingRouteNode2(
13661
+ graph,
13662
+ mappedService,
13663
+ method,
13664
+ normalizePathTemplate(path62)
13665
+ );
13666
+ if (routeNodeId) {
13667
+ return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types61.EdgeType.CALLS };
13668
+ }
13669
+ }
13670
+ return {
13671
+ targetNodeId: (0, import_types61.infraId)(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
13672
+ serviceName: mappedService ?? gcpServiceName,
13673
+ edgeType: import_types61.EdgeType.CALLS,
13674
+ ensureInfraNode: {
13675
+ kind: CLOUD_RUN_SERVICE_INFRA_KIND,
13676
+ name: gcpServiceName,
13677
+ provider: "cloud-run"
13678
+ }
13679
+ };
13680
+ };
13681
+ }
13682
+
13683
+ // src/connectors/cloud-run/index.ts
13684
+ var CloudRunConnector = class {
13685
+ constructor(config = {}) {
13686
+ this.config = config;
13687
+ }
13688
+ config;
13689
+ provider = "cloud-run";
13690
+ async poll(ctx) {
13691
+ const creds = readCloudRunCredentials(ctx.credentials);
13692
+ const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_LOOKBACK_MS2;
13693
+ const sinceIso = boundedSinceIso(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
13694
+ const entries = await fetchCloudRunRequestLogEntries(creds, sinceIso, this.config.apiUrl);
13695
+ return mapLogEntriesToSignals2(entries);
13696
+ }
13697
+ };
13698
+ function boundedSinceIso(since, now, maxLookbackMs) {
13699
+ const floor = new Date(now.getTime() - maxLookbackMs);
13700
+ if (!since) return floor.toISOString();
13701
+ const sinceMs = new Date(since).getTime();
13702
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
13703
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
13704
+ }
13705
+ function createCloudRunConnector(graph, config = {}) {
13706
+ return {
13707
+ connector: new CloudRunConnector(config),
13708
+ resolveTarget: createCloudRunResolveTarget(graph, config)
13709
+ };
13710
+ }
13711
+
13712
+ // src/connectors/render/index.ts
13713
+ init_cjs_shims();
13714
+ var import_types64 = require("@neat.is/types");
13715
+
13716
+ // src/connectors/render/types.ts
13717
+ init_cjs_shims();
13718
+ function readRenderToken(credentials) {
13719
+ const token = credentials.token;
13720
+ if (typeof token !== "string" || token.length === 0) {
13721
+ throw new Error("Render connector requires ctx.credentials.token (a Render API key)");
13722
+ }
13723
+ return token;
13724
+ }
13725
+ function renderLabelValue(entry2, name) {
13726
+ if (!Array.isArray(entry2.labels)) return void 0;
13727
+ const label = entry2.labels.find((l) => l && typeof l === "object" && l.name === name);
13728
+ return label && typeof label.value === "string" ? label.value : void 0;
13729
+ }
13730
+
13731
+ // src/connectors/render/client.ts
13732
+ init_cjs_shims();
13733
+ var DEFAULT_RENDER_API_URL = "https://api.render.com/v1";
13734
+ var DEFAULT_RENDER_LOG_LIMIT = 100;
13735
+ var RENDER_MAX_LOG_LIMIT = 100;
13736
+ var DEFAULT_RENDER_MAX_PAGES = 20;
13737
+ var DEFAULT_MAX_LOOKBACK_MS4 = 24 * 60 * 60 * 1e3;
13738
+ function clampLimit(limit) {
13739
+ const raw = Math.trunc(limit ?? DEFAULT_RENDER_LOG_LIMIT);
13740
+ if (!Number.isFinite(raw) || raw < 1) return DEFAULT_RENDER_LOG_LIMIT;
13741
+ return Math.min(raw, RENDER_MAX_LOG_LIMIT);
13742
+ }
13743
+ function boundedRenderStartTime(since, now, maxLookbackMs) {
13744
+ const floor = new Date(now.getTime() - maxLookbackMs);
13745
+ if (!since) return floor.toISOString();
13746
+ const sinceMs = new Date(since).getTime();
13747
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
13748
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
13749
+ }
13750
+ async function fetchRenderLogPage(config, token, startTime, endTime, limit, fetchImpl) {
13751
+ const url = new URL(`${config.apiUrl ?? DEFAULT_RENDER_API_URL}/logs`);
13752
+ url.searchParams.set("ownerId", config.ownerId);
13753
+ url.searchParams.set("resource", config.resourceId);
13754
+ url.searchParams.set("type", "request");
13755
+ url.searchParams.set("startTime", startTime);
13756
+ url.searchParams.set("endTime", endTime);
13757
+ url.searchParams.set("direction", "backward");
13758
+ url.searchParams.set("limit", String(limit));
13759
+ const res = await junctionFetch(
13760
+ url,
13761
+ { method: "GET", headers: { ...bearerAuthHeader(token) } },
13762
+ { provider: "render", accountKey: config.ownerId, ...fetchImpl ? { fetchImpl } : {} }
13763
+ );
13764
+ if (!res.ok) {
13765
+ throw new Error(`Render logs request failed: ${res.status} ${res.statusText}`);
13766
+ }
13767
+ return await res.json();
13768
+ }
13769
+ async function fetchRenderRequestLogs(config, token, startTime, endTime, fetchImpl) {
13770
+ const limit = clampLimit(config.limit);
13771
+ const maxPages = Math.max(1, Math.trunc(config.maxPages ?? DEFAULT_RENDER_MAX_PAGES));
13772
+ const out = [];
13773
+ let pageStart = startTime;
13774
+ let pageEnd = endTime;
13775
+ for (let page = 0; page < maxPages; page++) {
13776
+ const body = await fetchRenderLogPage(config, token, pageStart, pageEnd, limit, fetchImpl);
13777
+ if (Array.isArray(body.logs)) out.push(...body.logs);
13778
+ if (!body.hasMore || !body.nextStartTime || !body.nextEndTime) break;
13779
+ pageStart = body.nextStartTime;
13780
+ pageEnd = body.nextEndTime;
13781
+ }
13782
+ return out;
13783
+ }
13784
+
13785
+ // src/connectors/render/index.ts
13786
+ var ROUTE_TARGET_KIND2 = "route";
13787
+ var UNMATCHED_ROUTE_TARGET_KIND2 = "unmatched-route";
13788
+ function buildRenderRouteIndex(graph, serviceName) {
13789
+ const out = [];
13790
+ graph.forEachNode((_id, attrs) => {
13791
+ const node = attrs;
13792
+ if (node.type !== import_types64.NodeType.RouteNode) return;
13793
+ const route = attrs;
13794
+ if (route.service !== serviceName) return;
13795
+ out.push({
13796
+ method: route.method.toUpperCase(),
13797
+ normalizedPath: normalizePathTemplate(route.pathTemplate),
13798
+ routeNodeId: route.id,
13799
+ path: route.path,
13800
+ line: route.line
13801
+ });
13802
+ });
13803
+ return out;
13804
+ }
13805
+ function findRenderRoute(entries, method, normalizedPath) {
13806
+ return entries.find(
13807
+ (e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || e.method === method)
13808
+ );
13809
+ }
13810
+ function bucketKey3(method, normalizedPath) {
13811
+ return `${method} ${normalizedPath}`;
13812
+ }
13813
+ function isHttpErrorStatus2(status2) {
13814
+ return status2 >= 400;
13815
+ }
13816
+ function upsertBucket2(buckets2, key, isError, timestamp, build) {
13817
+ const existing = buckets2.get(key);
13818
+ if (existing) {
13819
+ existing.callCount += 1;
13820
+ if (isError) existing.errorCount += 1;
13821
+ if (timestamp > existing.lastObservedIso) existing.lastObservedIso = timestamp;
13822
+ return;
13823
+ }
13824
+ buckets2.set(key, { callCount: 1, errorCount: isError ? 1 : 0, lastObservedIso: timestamp, ...build() });
13825
+ }
13826
+ function mapRenderRequestLogsToSignals(entries, routeIndex) {
13827
+ const buckets2 = /* @__PURE__ */ new Map();
13828
+ if (!Array.isArray(entries)) return [];
13829
+ for (const entry2 of entries) {
13830
+ if (!entry2 || typeof entry2 !== "object") continue;
13831
+ if (typeof entry2.timestamp !== "string") continue;
13832
+ const method = renderLabelValue(entry2, "method");
13833
+ const rawPath = renderLabelValue(entry2, "path");
13834
+ if (typeof method !== "string" || method.length === 0) continue;
13835
+ if (typeof rawPath !== "string" || rawPath.length === 0) continue;
13836
+ const methodUpper = method.toUpperCase();
13837
+ const pathOnly = rawPath.split("?")[0];
13838
+ const normalizedPath = normalizePathTemplate(pathOnly);
13839
+ const statusCode = Number.parseInt(renderLabelValue(entry2, "statusCode") ?? "", 10);
13840
+ const isError = Number.isFinite(statusCode) && isHttpErrorStatus2(statusCode);
13841
+ const match = findRenderRoute(routeIndex, methodUpper, normalizedPath);
13842
+ if (match) {
13843
+ upsertBucket2(buckets2, `route:${match.routeNodeId}`, isError, entry2.timestamp, () => ({
13844
+ targetKind: ROUTE_TARGET_KIND2,
13845
+ targetName: match.routeNodeId,
13846
+ // RouteNode.line is optional in the schema (packages/types/src/
13847
+ // nodes.ts) even though routes.ts always sets it today — skip the
13848
+ // callSite rather than fabricate a line when it's ever absent
13849
+ // (file-awareness.md §6).
13850
+ ...match.line !== void 0 ? { callSite: { file: match.path, line: match.line } } : {}
13851
+ }));
13852
+ } else {
13853
+ upsertBucket2(
13854
+ buckets2,
13855
+ `unmatched:${bucketKey3(methodUpper, normalizedPath)}`,
13856
+ isError,
13857
+ entry2.timestamp,
13858
+ () => ({
13859
+ targetKind: UNMATCHED_ROUTE_TARGET_KIND2,
13860
+ targetName: bucketKey3(methodUpper, normalizedPath)
13861
+ })
13862
+ );
13863
+ }
13864
+ }
13865
+ return [...buckets2.values()].map((b) => ({
13866
+ targetKind: b.targetKind,
13867
+ targetName: b.targetName,
13868
+ callCount: b.callCount,
13869
+ errorCount: b.errorCount,
13870
+ lastObservedIso: b.lastObservedIso,
13871
+ ...b.callSite ? { callSite: b.callSite } : {}
13872
+ }));
13873
+ }
13874
+ function createRenderResolveTarget(config) {
13875
+ return (signal) => {
13876
+ if (signal.targetKind === ROUTE_TARGET_KIND2) {
13877
+ return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types64.EdgeType.CALLS };
13878
+ }
13879
+ return null;
13880
+ };
13881
+ }
13882
+ function createRenderConnector(graph, config) {
13883
+ return {
13884
+ provider: "render",
13885
+ async poll(ctx) {
13886
+ const token = readRenderToken(ctx.credentials);
13887
+ const now = /* @__PURE__ */ new Date();
13888
+ const maxLookbackMs = config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS4;
13889
+ const startTime = boundedRenderStartTime(ctx.since, now, maxLookbackMs);
13890
+ const endTime = now.toISOString();
13891
+ const logs = await fetchRenderRequestLogs(config, token, startTime, endTime);
13892
+ const routeIndex = buildRenderRouteIndex(graph, config.serviceName);
13893
+ return mapRenderRequestLogsToSignals(logs, routeIndex);
13894
+ }
13895
+ };
13896
+ }
13897
+
13204
13898
  // src/connectors/registry.ts
13205
13899
  var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
13206
13900
  async function authProbe(input) {
@@ -13376,6 +14070,71 @@ var PROVIDER_DISPATCH = {
13376
14070
  return { ok: false, reason: `neon telemetry read failed: ${err.message}` };
13377
14071
  }
13378
14072
  }
14073
+ },
14074
+ "cloud-run": {
14075
+ provider: "cloud-run",
14076
+ // Cloud Run reads both projectId and accessToken from the credential; the
14077
+ // single-string form maps to the secret (the token), and the required-fields
14078
+ // check below catches a projectId that was never supplied.
14079
+ primaryCredentialKey: "accessToken",
14080
+ requiredCredentialFields: ["projectId", "accessToken"],
14081
+ requiredOptionFields: [],
14082
+ build(graph, options) {
14083
+ return createCloudRunConnector(graph, options);
14084
+ },
14085
+ // POST entries:list with pageSize 1 — the exact surface poll() reads, so the
14086
+ // probe checks the actual `logging.logEntries.list` permission the connector
14087
+ // needs. A GET on the lighter logs.list endpoint (as Firebase probes) would
14088
+ // instead check `logging.logs.list`, falsely rejecting a correctly-scoped
14089
+ // custom role that carries only `logging.logEntries.list` (the narrowest
14090
+ // grant docs/connectors/cloud-run.md documents) — the same false-negative
14091
+ // trap Railway's validate avoids by probing its real query. A 2xx means the
14092
+ // token can list log entries; 401/403 means the provider rejected it.
14093
+ validate({ credentials, fetchImpl }) {
14094
+ const projectId = String(credentials.projectId ?? "");
14095
+ return authProbe({
14096
+ provider: "cloud-run",
14097
+ accountKey: projectId || "validate",
14098
+ url: "https://logging.googleapis.com/v2/entries:list",
14099
+ token: String(credentials.accessToken ?? ""),
14100
+ init: {
14101
+ method: "POST",
14102
+ headers: { "Content-Type": "application/json" },
14103
+ body: JSON.stringify({ resourceNames: [`projects/${projectId}`], pageSize: 1 })
14104
+ },
14105
+ ...fetchImpl ? { fetchImpl } : {}
14106
+ });
14107
+ }
14108
+ },
14109
+ render: {
14110
+ provider: "render",
14111
+ primaryCredentialKey: "token",
14112
+ requiredCredentialFields: ["token"],
14113
+ requiredOptionFields: ["ownerId", "resourceId", "serviceName"],
14114
+ build(graph, options) {
14115
+ const config = options;
14116
+ return {
14117
+ connector: createRenderConnector(graph, config),
14118
+ resolveTarget: createRenderResolveTarget(config)
14119
+ };
14120
+ },
14121
+ // GET /v1/services?limit=1 — the cheapest read the Render API key
14122
+ // authenticates against (render.com/docs/api). Unlike Railway's GraphQL
14123
+ // gateway, Render is a plain REST API: a live key returns 2xx, a bad one a
14124
+ // 401/403, so authProbe's status-code check is a true verdict here. The
14125
+ // logs query itself also needs an ownerId + resource; `services` needs
14126
+ // neither and still fails 401 on a bad token, so it's the honest probe.
14127
+ validate({ credentials, options, fetchImpl }) {
14128
+ const cfg = options;
14129
+ const baseUrl = cfg.apiUrl ?? DEFAULT_RENDER_API_URL;
14130
+ return authProbe({
14131
+ provider: "render",
14132
+ accountKey: cfg.ownerId ?? "validate",
14133
+ url: `${baseUrl}/services?limit=1`,
14134
+ token: String(credentials.token ?? ""),
14135
+ ...fetchImpl ? { fetchImpl } : {}
14136
+ });
14137
+ }
13379
14138
  }
13380
14139
  };
13381
14140
  function vercelCredsFrom(credentials) {
@@ -13727,11 +14486,11 @@ function registerRoutes(scope, ctx) {
13727
14486
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
13728
14487
  const parsed = [];
13729
14488
  for (const c of candidates) {
13730
- const r = import_types59.DivergenceTypeSchema.safeParse(c);
14489
+ const r = import_types66.DivergenceTypeSchema.safeParse(c);
13731
14490
  if (!r.success) {
13732
14491
  return reply.code(400).send({
13733
14492
  error: `unknown divergence type "${c}"`,
13734
- allowed: import_types59.DivergenceTypeSchema.options
14493
+ allowed: import_types66.DivergenceTypeSchema.options
13735
14494
  });
13736
14495
  }
13737
14496
  parsed.push(r.data);
@@ -14040,7 +14799,7 @@ function registerRoutes(scope, ctx) {
14040
14799
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
14041
14800
  let violations = await log.readAll();
14042
14801
  if (req2.query.severity) {
14043
- const sev = import_types59.PolicySeveritySchema.safeParse(req2.query.severity);
14802
+ const sev = import_types66.PolicySeveritySchema.safeParse(req2.query.severity);
14044
14803
  if (!sev.success) {
14045
14804
  return reply.code(400).send({
14046
14805
  error: "invalid severity",
@@ -14079,7 +14838,7 @@ function registerRoutes(scope, ctx) {
14079
14838
  scope.post("/policies/check", async (req2, reply) => {
14080
14839
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
14081
14840
  if (!proj) return;
14082
- const parsed = import_types59.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
14841
+ const parsed = import_types66.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
14083
14842
  if (!parsed.success) {
14084
14843
  return reply.code(400).send({
14085
14844
  error: "invalid /policies/check body",
@@ -14420,7 +15179,7 @@ function unroutedErrorsPath(neatHome4) {
14420
15179
  }
14421
15180
 
14422
15181
  // src/daemon.ts
14423
- var import_types60 = require("@neat.is/types");
15182
+ var import_types67 = require("@neat.is/types");
14424
15183
  function daemonJsonPath(scanPath) {
14425
15184
  return import_node_path59.default.join(scanPath, "neat-out", "daemon.json");
14426
15185
  }
@@ -14559,7 +15318,7 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
14559
15318
  if (!serviceName) return true;
14560
15319
  if (serviceNameMatchesProject(serviceName, project)) return true;
14561
15320
  return graph.someNode(
14562
- (_id, attrs) => attrs.type === import_types60.NodeType.ServiceNode && attrs.name === serviceName
15321
+ (_id, attrs) => attrs.type === import_types67.NodeType.ServiceNode && attrs.name === serviceName
14563
15322
  );
14564
15323
  }
14565
15324
  async function bootstrapProject(entry2, connectors = [], neatHome4) {