@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/index.cjs CHANGED
@@ -8373,6 +8373,55 @@ function columnsFromClassBody(body) {
8373
8373
  }
8374
8374
  return out;
8375
8375
  }
8376
+ function foreignKeyParentTable(call) {
8377
+ const fn = call.childForFieldName("function");
8378
+ const t = fn?.text;
8379
+ if (!t) return null;
8380
+ const base = t.includes(".") ? t.slice(t.lastIndexOf(".") + 1) : t;
8381
+ if (base !== "ForeignKey") return null;
8382
+ const target = firstPositionalString(call);
8383
+ if (!target) return null;
8384
+ const parts = target.split(".");
8385
+ if (parts.length < 2) return null;
8386
+ return parts[parts.length - 2];
8387
+ }
8388
+ function sqlalchemyForeignKeys(file, serviceDir) {
8389
+ if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
8390
+ const tree = parseSource6(makePyParser4(), file.content);
8391
+ const out = [];
8392
+ const seen = /* @__PURE__ */ new Set();
8393
+ walk3(tree.rootNode, (node) => {
8394
+ if (node.type !== "class_definition") return;
8395
+ const body = node.childForFieldName("body");
8396
+ const nameNode = node.childForFieldName("name");
8397
+ if (!body || !nameNode) return;
8398
+ const explicit = explicitTablename(body);
8399
+ if (explicit === "computed") return;
8400
+ let childTable = null;
8401
+ if (explicit) childTable = explicit.name;
8402
+ else if (extendsFlaskModel(node)) childTable = flaskSqlalchemyTableName(nameNode.text);
8403
+ if (!childTable) return;
8404
+ walk3(body, (n) => {
8405
+ if (n.type !== "call") return;
8406
+ const parentTable = foreignKeyParentTable(n);
8407
+ if (!parentTable) return;
8408
+ const key = `${childTable}->${parentTable}`;
8409
+ if (seen.has(key)) return;
8410
+ seen.add(key);
8411
+ const line = n.startPosition.row + 1;
8412
+ out.push({
8413
+ childTable,
8414
+ parentTable,
8415
+ evidence: {
8416
+ file: import_node_path35.default.relative(serviceDir, file.path),
8417
+ line,
8418
+ snippet: snippet(file.content, line)
8419
+ }
8420
+ });
8421
+ });
8422
+ });
8423
+ return out;
8424
+ }
8376
8425
  function sqlalchemyEndpointsFromFile(file, serviceDir) {
8377
8426
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
8378
8427
  const tree = parseSource6(makePyParser4(), file.content);
@@ -8718,6 +8767,92 @@ function drizzleEndpointsFromFile(file, serviceDir) {
8718
8767
  walk6(tree.rootNode);
8719
8768
  return out;
8720
8769
  }
8770
+ function enclosingVarName(call) {
8771
+ let node = call;
8772
+ while (node?.parent) {
8773
+ const parent = node.parent;
8774
+ if (parent.type === "variable_declarator") {
8775
+ const name = parent.childForFieldName("name");
8776
+ return name?.type === "identifier" ? name.text : null;
8777
+ }
8778
+ if (parent.type === "call_expression" || parent.type === "member_expression") {
8779
+ node = parent;
8780
+ continue;
8781
+ }
8782
+ return null;
8783
+ }
8784
+ return null;
8785
+ }
8786
+ function collectDrizzleTables(root) {
8787
+ const tables = [];
8788
+ const varToTable = /* @__PURE__ */ new Map();
8789
+ const walk6 = (node) => {
8790
+ if (node.type === "call_expression") {
8791
+ const fn = node.childForFieldName("function");
8792
+ if (fn?.type === "identifier" && TABLE_BUILDERS.has(fn.text)) {
8793
+ const args = node.childForFieldName("arguments");
8794
+ const argNodes = args ? namedChildren3(args) : [];
8795
+ const tableName = stringLiteralText2(argNodes[0] ?? null);
8796
+ const obj = argNodes[1]?.type === "object" ? argNodes[1] : null;
8797
+ if (tableName) {
8798
+ tables.push({ tableName, object: obj });
8799
+ const varName = enclosingVarName(node);
8800
+ if (varName) varToTable.set(varName, tableName);
8801
+ }
8802
+ }
8803
+ }
8804
+ for (const c of namedChildren3(node)) walk6(c);
8805
+ };
8806
+ walk6(root);
8807
+ return { tables, varToTable };
8808
+ }
8809
+ function referencesTargetVar(call) {
8810
+ const fn = call.childForFieldName("function");
8811
+ if (fn?.type !== "member_expression") return null;
8812
+ if (fn.childForFieldName("property")?.text !== "references") return null;
8813
+ const args = call.childForFieldName("arguments");
8814
+ const first = args ? namedChildren3(args)[0] ?? null : null;
8815
+ if (first?.type !== "arrow_function") return null;
8816
+ const body = first.childForFieldName("body");
8817
+ if (body?.type !== "member_expression") return null;
8818
+ const obj = body.childForFieldName("object");
8819
+ return obj?.type === "identifier" ? obj.text : null;
8820
+ }
8821
+ function drizzleForeignKeys(file, serviceDir) {
8822
+ if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
8823
+ const tree = parseSource3(parserForExt(import_node_path37.default.extname(file.path)), file.content);
8824
+ const { tables, varToTable } = collectDrizzleTables(tree.rootNode);
8825
+ const out = [];
8826
+ const seen = /* @__PURE__ */ new Set();
8827
+ for (const table of tables) {
8828
+ if (!table.object) continue;
8829
+ const walk6 = (node) => {
8830
+ if (node.type === "call_expression") {
8831
+ const targetVar = referencesTargetVar(node);
8832
+ const parentTable = targetVar ? varToTable.get(targetVar) : void 0;
8833
+ if (parentTable) {
8834
+ const key = `${table.tableName}->${parentTable}`;
8835
+ if (!seen.has(key)) {
8836
+ seen.add(key);
8837
+ const line = node.startPosition.row + 1;
8838
+ out.push({
8839
+ childTable: table.tableName,
8840
+ parentTable,
8841
+ evidence: {
8842
+ file: import_node_path37.default.relative(serviceDir, file.path),
8843
+ line,
8844
+ snippet: snippet(file.content, line)
8845
+ }
8846
+ });
8847
+ }
8848
+ }
8849
+ }
8850
+ for (const c of namedChildren3(node)) walk6(c);
8851
+ };
8852
+ walk6(table.object);
8853
+ }
8854
+ return out;
8855
+ }
8721
8856
 
8722
8857
  // src/extract/calls/prisma.ts
8723
8858
  init_cjs_shims();
@@ -8846,6 +8981,96 @@ async function prismaColumnEndpoints(serviceDir) {
8846
8981
  if (!content) return [];
8847
8982
  return prismaColumnsFromSchema({ path: schemaPath, content }, serviceDir);
8848
8983
  }
8984
+ function buildModelTableMap(lines) {
8985
+ const map = /* @__PURE__ */ new Map();
8986
+ let current = null;
8987
+ let depth = 0;
8988
+ for (const raw of lines) {
8989
+ if (current === null) {
8990
+ const header = raw.match(/^\s*model\s+([A-Za-z_]\w*)\b/);
8991
+ if (header && raw.includes("{")) {
8992
+ current = { model: header[1], table: header[1] };
8993
+ depth = netBraces(raw);
8994
+ if (depth <= 0) {
8995
+ map.set(current.model, current.table);
8996
+ current = null;
8997
+ }
8998
+ }
8999
+ continue;
9000
+ }
9001
+ depth += netBraces(raw);
9002
+ const trimmed = stripLineComment(raw).trim();
9003
+ if (trimmed.startsWith("@@")) {
9004
+ const m = trimmed.match(/@@map\(\s*"([^"]+)"\s*\)/);
9005
+ if (m) current.table = m[1];
9006
+ }
9007
+ if (depth <= 0) {
9008
+ map.set(current.model, current.table);
9009
+ current = null;
9010
+ }
9011
+ }
9012
+ if (current) map.set(current.model, current.table);
9013
+ return map;
9014
+ }
9015
+ function prismaForeignKeysFromSchema(file, serviceDir) {
9016
+ const content = file.content;
9017
+ if (!/\bmodel\s+[A-Za-z_]\w*\s*\{/.test(content)) return [];
9018
+ const lines = content.split("\n");
9019
+ const modelToTable = buildModelTableMap(lines);
9020
+ const out = [];
9021
+ const seen = /* @__PURE__ */ new Set();
9022
+ let current = null;
9023
+ let depth = 0;
9024
+ for (let i = 0; i < lines.length; i++) {
9025
+ const raw = lines[i];
9026
+ const lineNo = i + 1;
9027
+ if (current === null) {
9028
+ const header = raw.match(/^\s*model\s+([A-Za-z_]\w*)\b/);
9029
+ if (header && raw.includes("{")) {
9030
+ current = { table: modelToTable.get(header[1]) ?? header[1] };
9031
+ depth = netBraces(raw);
9032
+ if (depth <= 0) current = null;
9033
+ }
9034
+ continue;
9035
+ }
9036
+ depth += netBraces(raw);
9037
+ const closing = depth <= 0;
9038
+ const trimmed = stripLineComment(raw).trim();
9039
+ if (trimmed && !trimmed.startsWith("@@") && !trimmed.startsWith("}")) {
9040
+ const fm = trimmed.match(/^([A-Za-z_]\w*)\s+([A-Za-z_]\w*)/);
9041
+ if (fm && /@relation\b[^)]*\bfields\s*:/.test(trimmed)) {
9042
+ const parentTable = modelToTable.get(fm[2]);
9043
+ if (parentTable) {
9044
+ const key = `${current.table}->${parentTable}`;
9045
+ if (!seen.has(key)) {
9046
+ seen.add(key);
9047
+ out.push({
9048
+ childTable: current.table,
9049
+ parentTable,
9050
+ evidence: {
9051
+ file: import_node_path38.default.relative(serviceDir, file.path),
9052
+ line: lineNo,
9053
+ snippet: snippet(content, lineNo)
9054
+ }
9055
+ });
9056
+ }
9057
+ }
9058
+ }
9059
+ }
9060
+ if (closing) current = null;
9061
+ }
9062
+ return out;
9063
+ }
9064
+ async function prismaForeignKeys(serviceDir) {
9065
+ const schemaPath = await findFirst(serviceDir, [
9066
+ import_node_path38.default.join("prisma", "schema.prisma"),
9067
+ "schema.prisma"
9068
+ ]);
9069
+ if (!schemaPath) return [];
9070
+ const content = await readIfExists(schemaPath);
9071
+ if (!content) return [];
9072
+ return prismaForeignKeysFromSchema({ path: schemaPath, content }, serviceDir);
9073
+ }
8849
9074
 
8850
9075
  // src/extract/calls/go.ts
8851
9076
  init_cjs_shims();
@@ -9022,21 +9247,79 @@ async function addCallEdges(graph, services) {
9022
9247
  };
9023
9248
  }
9024
9249
 
9250
+ // src/extract/table-edges.ts
9251
+ init_cjs_shims();
9252
+ var import_types31 = require("@neat.is/types");
9253
+ async function addTableEdges(graph, services) {
9254
+ let nodesAdded = 0;
9255
+ let edgesAdded = 0;
9256
+ for (const service of services) {
9257
+ const files = await loadSourceFiles(service.dir);
9258
+ const refs = [];
9259
+ for (const file of files) {
9260
+ try {
9261
+ refs.push(...drizzleForeignKeys(file, service.dir));
9262
+ refs.push(...sqlalchemyForeignKeys(file, service.dir));
9263
+ } catch (err) {
9264
+ recordExtractionError("foreign-key extraction", file.path, err);
9265
+ }
9266
+ }
9267
+ try {
9268
+ refs.push(...await prismaForeignKeys(service.dir));
9269
+ } catch (err) {
9270
+ recordExtractionError("prisma foreign-key extraction", service.dir, err);
9271
+ }
9272
+ for (const ref of refs) {
9273
+ const childId = (0, import_types31.infraId)("sql-table", ref.childTable);
9274
+ const parentId = (0, import_types31.infraId)("sql-table", ref.parentTable);
9275
+ if (childId === parentId) continue;
9276
+ nodesAdded += ensureTableNode(graph, childId, ref.childTable);
9277
+ nodesAdded += ensureTableNode(graph, parentId, ref.parentTable);
9278
+ const edgeId = (0, import_types31.extractedEdgeId)(childId, parentId, import_types31.EdgeType.REFERENCES);
9279
+ if (graph.hasEdge(edgeId)) continue;
9280
+ const edge = {
9281
+ id: edgeId,
9282
+ source: childId,
9283
+ target: parentId,
9284
+ type: import_types31.EdgeType.REFERENCES,
9285
+ provenance: import_types31.Provenance.EXTRACTED,
9286
+ confidence: (0, import_types31.confidenceForExtracted)("structural"),
9287
+ evidence: ref.evidence
9288
+ };
9289
+ graph.addEdgeWithKey(edgeId, childId, parentId, edge);
9290
+ edgesAdded++;
9291
+ }
9292
+ }
9293
+ return { nodesAdded, edgesAdded };
9294
+ }
9295
+ function ensureTableNode(graph, id, name) {
9296
+ if (graph.hasNode(id)) return 0;
9297
+ const node = {
9298
+ id,
9299
+ type: import_types31.NodeType.InfraNode,
9300
+ name,
9301
+ provider: "self",
9302
+ kind: "sql-table"
9303
+ };
9304
+ graph.addNode(id, node);
9305
+ return 1;
9306
+ }
9307
+
9025
9308
  // src/extract/infra/index.ts
9026
9309
  init_cjs_shims();
9027
9310
 
9028
9311
  // src/extract/infra/docker-compose.ts
9029
9312
  init_cjs_shims();
9030
9313
  var import_node_path42 = __toESM(require("path"), 1);
9031
- var import_types32 = require("@neat.is/types");
9314
+ var import_types33 = require("@neat.is/types");
9032
9315
 
9033
9316
  // src/extract/infra/shared.ts
9034
9317
  init_cjs_shims();
9035
- var import_types31 = require("@neat.is/types");
9318
+ var import_types32 = require("@neat.is/types");
9036
9319
  function makeInfraNode(kind, name, provider = "self", extras) {
9037
9320
  return {
9038
- id: (0, import_types31.infraId)(kind, name),
9039
- type: import_types31.NodeType.InfraNode,
9321
+ id: (0, import_types32.infraId)(kind, name),
9322
+ type: import_types32.NodeType.InfraNode,
9040
9323
  name,
9041
9324
  provider,
9042
9325
  kind,
@@ -9080,8 +9363,8 @@ function emitPlatformResourceEdge(graph, anchorId, edgeType, kind, name, provide
9080
9363
  source: anchorId,
9081
9364
  target: node.id,
9082
9365
  type: edgeType,
9083
- provenance: import_types31.Provenance.EXTRACTED,
9084
- confidence: (0, import_types31.confidenceForExtracted)("structural"),
9366
+ provenance: import_types32.Provenance.EXTRACTED,
9367
+ confidence: (0, import_types32.confidenceForExtracted)("structural"),
9085
9368
  evidence: { file: evidenceFile, ...line !== void 0 ? { line } : {} }
9086
9369
  };
9087
9370
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -9148,15 +9431,15 @@ async function addComposeInfra(graph, scanPath, services) {
9148
9431
  for (const dep of dependsOnList(svc.depends_on)) {
9149
9432
  const targetId = composeNameToNodeId.get(dep);
9150
9433
  if (!targetId) continue;
9151
- const edgeId = (0, import_types3.extractedEdgeId)(sourceId, targetId, import_types32.EdgeType.DEPENDS_ON);
9434
+ const edgeId = (0, import_types3.extractedEdgeId)(sourceId, targetId, import_types33.EdgeType.DEPENDS_ON);
9152
9435
  if (graph.hasEdge(edgeId)) continue;
9153
9436
  const edge = {
9154
9437
  id: edgeId,
9155
9438
  source: sourceId,
9156
9439
  target: targetId,
9157
- type: import_types32.EdgeType.DEPENDS_ON,
9158
- provenance: import_types32.Provenance.EXTRACTED,
9159
- confidence: (0, import_types32.confidenceForExtracted)("structural"),
9440
+ type: import_types33.EdgeType.DEPENDS_ON,
9441
+ provenance: import_types33.Provenance.EXTRACTED,
9442
+ confidence: (0, import_types33.confidenceForExtracted)("structural"),
9160
9443
  evidence: { file: evidenceFile }
9161
9444
  };
9162
9445
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -9170,7 +9453,7 @@ async function addComposeInfra(graph, scanPath, services) {
9170
9453
  init_cjs_shims();
9171
9454
  var import_node_path43 = __toESM(require("path"), 1);
9172
9455
  var import_node_fs17 = require("fs");
9173
- var import_types33 = require("@neat.is/types");
9456
+ var import_types34 = require("@neat.is/types");
9174
9457
  function readDockerfile(content) {
9175
9458
  let image = null;
9176
9459
  const ports = [];
@@ -9229,15 +9512,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
9229
9512
  );
9230
9513
  nodesAdded += fn;
9231
9514
  edgesAdded += fe;
9232
- const edgeId = (0, import_types3.extractedEdgeId)(fileNodeId, node.id, import_types33.EdgeType.RUNS_ON);
9515
+ const edgeId = (0, import_types3.extractedEdgeId)(fileNodeId, node.id, import_types34.EdgeType.RUNS_ON);
9233
9516
  if (!graph.hasEdge(edgeId)) {
9234
9517
  const edge = {
9235
9518
  id: edgeId,
9236
9519
  source: fileNodeId,
9237
9520
  target: node.id,
9238
- type: import_types33.EdgeType.RUNS_ON,
9239
- provenance: import_types33.Provenance.EXTRACTED,
9240
- confidence: (0, import_types33.confidenceForExtracted)("structural"),
9521
+ type: import_types34.EdgeType.RUNS_ON,
9522
+ provenance: import_types34.Provenance.EXTRACTED,
9523
+ confidence: (0, import_types34.confidenceForExtracted)("structural"),
9241
9524
  evidence: {
9242
9525
  file: evidenceFile,
9243
9526
  ...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
@@ -9252,15 +9535,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
9252
9535
  graph.addNode(portNode.id, portNode);
9253
9536
  nodesAdded++;
9254
9537
  }
9255
- const portEdgeId = (0, import_types3.extractedEdgeId)(fileNodeId, portNode.id, import_types33.EdgeType.CONNECTS_TO);
9538
+ const portEdgeId = (0, import_types3.extractedEdgeId)(fileNodeId, portNode.id, import_types34.EdgeType.CONNECTS_TO);
9256
9539
  if (graph.hasEdge(portEdgeId)) continue;
9257
9540
  const portEdge = {
9258
9541
  id: portEdgeId,
9259
9542
  source: fileNodeId,
9260
9543
  target: portNode.id,
9261
- type: import_types33.EdgeType.CONNECTS_TO,
9262
- provenance: import_types33.Provenance.EXTRACTED,
9263
- confidence: (0, import_types33.confidenceForExtracted)("structural"),
9544
+ type: import_types34.EdgeType.CONNECTS_TO,
9545
+ provenance: import_types34.Provenance.EXTRACTED,
9546
+ confidence: (0, import_types34.confidenceForExtracted)("structural"),
9264
9547
  evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
9265
9548
  };
9266
9549
  graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
@@ -9274,7 +9557,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
9274
9557
  init_cjs_shims();
9275
9558
  var import_node_fs18 = require("fs");
9276
9559
  var import_node_path44 = __toESM(require("path"), 1);
9277
- var import_types34 = require("@neat.is/types");
9560
+ var import_types35 = require("@neat.is/types");
9278
9561
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
9279
9562
  var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
9280
9563
  async function walkTfFiles(start, depth = 0, max = 5) {
@@ -9355,16 +9638,16 @@ async function addTerraformResources(graph, scanPath) {
9355
9638
  if (!target) continue;
9356
9639
  if (seen.has(target.nodeId)) continue;
9357
9640
  seen.add(target.nodeId);
9358
- const edgeId = (0, import_types3.extractedEdgeId)(resource.nodeId, target.nodeId, import_types34.EdgeType.DEPENDS_ON);
9641
+ const edgeId = (0, import_types3.extractedEdgeId)(resource.nodeId, target.nodeId, import_types35.EdgeType.DEPENDS_ON);
9359
9642
  if (graph.hasEdge(edgeId)) continue;
9360
9643
  const line = lineAt2(content, resource.bodyOffset + ref.index);
9361
9644
  const edge = {
9362
9645
  id: edgeId,
9363
9646
  source: resource.nodeId,
9364
9647
  target: target.nodeId,
9365
- type: import_types34.EdgeType.DEPENDS_ON,
9366
- provenance: import_types34.Provenance.EXTRACTED,
9367
- confidence: (0, import_types34.confidenceForExtracted)("structural"),
9648
+ type: import_types35.EdgeType.DEPENDS_ON,
9649
+ provenance: import_types35.Provenance.EXTRACTED,
9650
+ confidence: (0, import_types35.confidenceForExtracted)("structural"),
9368
9651
  evidence: { file: evidenceFile, line, snippet: key }
9369
9652
  };
9370
9653
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -9436,7 +9719,7 @@ init_cjs_shims();
9436
9719
  var import_node_fs20 = require("fs");
9437
9720
  var import_node_path46 = __toESM(require("path"), 1);
9438
9721
  var import_smol_toml2 = require("smol-toml");
9439
- var import_types35 = require("@neat.is/types");
9722
+ var import_types36 = require("@neat.is/types");
9440
9723
  var WRANGLER_FILENAMES = ["wrangler.toml", "wrangler.jsonc", "wrangler.json"];
9441
9724
  async function readWranglerConfig(dir) {
9442
9725
  for (const filename of WRANGLER_FILENAMES) {
@@ -9484,8 +9767,8 @@ function addResourceEdge(graph, anchorId, edgeType, kind, name, evidenceFile, li
9484
9767
  source: anchorId,
9485
9768
  target: node.id,
9486
9769
  type: edgeType,
9487
- provenance: import_types35.Provenance.EXTRACTED,
9488
- confidence: (0, import_types35.confidenceForExtracted)("structural"),
9770
+ provenance: import_types36.Provenance.EXTRACTED,
9771
+ confidence: (0, import_types36.confidenceForExtracted)("structural"),
9489
9772
  evidence: { file: evidenceFile, ...line !== void 0 ? { line } : {} }
9490
9773
  };
9491
9774
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -9546,15 +9829,15 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9546
9829
  nodesAdded++;
9547
9830
  }
9548
9831
  if (runtimeNode.id !== anchorId) {
9549
- const runsOnId = (0, import_types3.extractedEdgeId)(anchorId, runtimeNode.id, import_types35.EdgeType.RUNS_ON);
9832
+ const runsOnId = (0, import_types3.extractedEdgeId)(anchorId, runtimeNode.id, import_types36.EdgeType.RUNS_ON);
9550
9833
  if (!graph.hasEdge(runsOnId)) {
9551
9834
  const edge = {
9552
9835
  id: runsOnId,
9553
9836
  source: anchorId,
9554
9837
  target: runtimeNode.id,
9555
- type: import_types35.EdgeType.RUNS_ON,
9556
- provenance: import_types35.Provenance.EXTRACTED,
9557
- confidence: (0, import_types35.confidenceForExtracted)("structural"),
9838
+ type: import_types36.EdgeType.RUNS_ON,
9839
+ provenance: import_types36.Provenance.EXTRACTED,
9840
+ confidence: (0, import_types36.confidenceForExtracted)("structural"),
9558
9841
  evidence: {
9559
9842
  file: evidenceFile,
9560
9843
  ...config.compatibility_date ? { snippet: `compatibility_date = ${config.compatibility_date}`.slice(0, 120) } : {}
@@ -9568,7 +9851,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9568
9851
  const result = addResourceEdge(
9569
9852
  graph,
9570
9853
  anchorId,
9571
- import_types35.EdgeType.CONNECTS_TO,
9854
+ import_types36.EdgeType.CONNECTS_TO,
9572
9855
  "cloudflare-route",
9573
9856
  route,
9574
9857
  evidenceFile,
@@ -9592,7 +9875,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9592
9875
  const result = addResourceEdge(
9593
9876
  graph,
9594
9877
  anchorId,
9595
- import_types35.EdgeType.DEPENDS_ON,
9878
+ import_types36.EdgeType.DEPENDS_ON,
9596
9879
  group.kind,
9597
9880
  name,
9598
9881
  evidenceFile,
@@ -9606,7 +9889,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9606
9889
  const result = addResourceEdge(
9607
9890
  graph,
9608
9891
  anchorId,
9609
- import_types35.EdgeType.DEPENDS_ON,
9892
+ import_types36.EdgeType.DEPENDS_ON,
9610
9893
  "cloudflare-cron",
9611
9894
  cron,
9612
9895
  evidenceFile,
@@ -9619,7 +9902,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9619
9902
  const result = addResourceEdge(
9620
9903
  graph,
9621
9904
  anchorId,
9622
- import_types35.EdgeType.DEPENDS_ON,
9905
+ import_types36.EdgeType.DEPENDS_ON,
9623
9906
  "cloudflare-env-var",
9624
9907
  varName,
9625
9908
  evidenceFile,
@@ -9632,15 +9915,15 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9632
9915
  if (!svc.service) continue;
9633
9916
  const target = workerIndex.get(svc.service);
9634
9917
  if (target && target.anchorId !== anchorId) {
9635
- const edgeId = (0, import_types3.extractedEdgeId)(anchorId, target.anchorId, import_types35.EdgeType.CALLS);
9918
+ const edgeId = (0, import_types3.extractedEdgeId)(anchorId, target.anchorId, import_types36.EdgeType.CALLS);
9636
9919
  if (!graph.hasEdge(edgeId)) {
9637
9920
  const edge = {
9638
9921
  id: edgeId,
9639
9922
  source: anchorId,
9640
9923
  target: target.anchorId,
9641
- type: import_types35.EdgeType.CALLS,
9642
- provenance: import_types35.Provenance.EXTRACTED,
9643
- confidence: (0, import_types35.confidenceForExtracted)("structural"),
9924
+ type: import_types36.EdgeType.CALLS,
9925
+ provenance: import_types36.Provenance.EXTRACTED,
9926
+ confidence: (0, import_types36.confidenceForExtracted)("structural"),
9644
9927
  evidence: { file: evidenceFile, line: lineContaining2(raw, svc.service) }
9645
9928
  };
9646
9929
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -9651,7 +9934,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9651
9934
  const result = addResourceEdge(
9652
9935
  graph,
9653
9936
  anchorId,
9654
- import_types35.EdgeType.DEPENDS_ON,
9937
+ import_types36.EdgeType.DEPENDS_ON,
9655
9938
  "cloudflare-service-binding",
9656
9939
  svc.service,
9657
9940
  evidenceFile,
@@ -9668,7 +9951,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
9668
9951
  init_cjs_shims();
9669
9952
  var import_node_fs21 = require("fs");
9670
9953
  var import_node_path47 = __toESM(require("path"), 1);
9671
- var import_types36 = require("@neat.is/types");
9954
+ var import_types37 = require("@neat.is/types");
9672
9955
  var VERCEL_CONFIG_FILENAMES = ["vercel.json", "vercel.jsonc"];
9673
9956
  async function readVercelConfig(dir) {
9674
9957
  for (const filename of VERCEL_CONFIG_FILENAMES) {
@@ -9731,12 +10014,12 @@ async function addVercelServices(graph, services, scanPath) {
9731
10014
  nodesAdded += result.nodesAdded;
9732
10015
  edgesAdded += result.edgesAdded;
9733
10016
  };
9734
- add(import_types36.EdgeType.RUNS_ON, "vercel", "vercel");
9735
- for (const cron of config.crons ?? []) add(import_types36.EdgeType.DEPENDS_ON, "vercel-cron", cron.path ?? cron.schedule);
9736
- for (const varName of Object.keys(config.env ?? {})) add(import_types36.EdgeType.DEPENDS_ON, "vercel-env-var", varName);
9737
- for (const varName of Object.keys(config.build?.env ?? {})) add(import_types36.EdgeType.DEPENDS_ON, "vercel-env-var", varName);
10017
+ add(import_types37.EdgeType.RUNS_ON, "vercel", "vercel");
10018
+ for (const cron of config.crons ?? []) add(import_types37.EdgeType.DEPENDS_ON, "vercel-cron", cron.path ?? cron.schedule);
10019
+ for (const varName of Object.keys(config.env ?? {})) add(import_types37.EdgeType.DEPENDS_ON, "vercel-env-var", varName);
10020
+ for (const varName of Object.keys(config.build?.env ?? {})) add(import_types37.EdgeType.DEPENDS_ON, "vercel-env-var", varName);
9738
10021
  for (const route of [...config.rewrites ?? [], ...config.redirects ?? [], ...config.routes ?? []]) {
9739
- add(import_types36.EdgeType.CONNECTS_TO, "vercel-route", routeSource(route));
10022
+ add(import_types37.EdgeType.CONNECTS_TO, "vercel-route", routeSource(route));
9740
10023
  }
9741
10024
  }
9742
10025
  return { nodesAdded, edgesAdded };
@@ -9747,7 +10030,7 @@ init_cjs_shims();
9747
10030
  var import_node_fs22 = require("fs");
9748
10031
  var import_node_path48 = __toESM(require("path"), 1);
9749
10032
  var import_smol_toml3 = require("smol-toml");
9750
- var import_types37 = require("@neat.is/types");
10033
+ var import_types38 = require("@neat.is/types");
9751
10034
  var RAILWAY_FILENAMES = ["railway.toml", "railway.json", "railway.jsonc"];
9752
10035
  async function readRailwayConfig(dir) {
9753
10036
  for (const filename of RAILWAY_FILENAMES) {
@@ -9793,9 +10076,9 @@ async function addRailwayServices(graph, services, scanPath) {
9793
10076
  nodesAdded += result.nodesAdded;
9794
10077
  edgesAdded += result.edgesAdded;
9795
10078
  };
9796
- add(import_types37.EdgeType.RUNS_ON, "railway", "railway");
9797
- add(import_types37.EdgeType.CONNECTS_TO, "railway-route", config.deploy?.healthcheckPath);
9798
- add(import_types37.EdgeType.DEPENDS_ON, "railway-cron", config.deploy?.cronSchedule);
10079
+ add(import_types38.EdgeType.RUNS_ON, "railway", "railway");
10080
+ add(import_types38.EdgeType.CONNECTS_TO, "railway-route", config.deploy?.healthcheckPath);
10081
+ add(import_types38.EdgeType.DEPENDS_ON, "railway-cron", config.deploy?.cronSchedule);
9799
10082
  }
9800
10083
  return { nodesAdded, edgesAdded };
9801
10084
  }
@@ -9805,7 +10088,7 @@ init_cjs_shims();
9805
10088
  var import_node_fs23 = require("fs");
9806
10089
  var import_node_path49 = __toESM(require("path"), 1);
9807
10090
  var import_smol_toml4 = require("smol-toml");
9808
- var import_types38 = require("@neat.is/types");
10091
+ var import_types39 = require("@neat.is/types");
9809
10092
  async function readSupabaseConfig(dir) {
9810
10093
  const relFile = import_node_path49.default.join("supabase", "config.toml");
9811
10094
  const abs = import_node_path49.default.join(dir, relFile);
@@ -9853,10 +10136,10 @@ async function addSupabaseProjects(graph, services, scanPath) {
9853
10136
  nodesAdded += result.nodesAdded;
9854
10137
  edgesAdded += result.edgesAdded;
9855
10138
  };
9856
- add(import_types38.EdgeType.RUNS_ON, "supabase", "supabase");
9857
- for (const fn of Object.keys(config.functions ?? {})) add(import_types38.EdgeType.DEPENDS_ON, "supabase-function", fn);
9858
- if (config.storage) add(import_types38.EdgeType.DEPENDS_ON, "supabase-storage", "storage");
9859
- if (config.auth) add(import_types38.EdgeType.DEPENDS_ON, "supabase-auth", "auth");
10139
+ add(import_types39.EdgeType.RUNS_ON, "supabase", "supabase");
10140
+ for (const fn of Object.keys(config.functions ?? {})) add(import_types39.EdgeType.DEPENDS_ON, "supabase-function", fn);
10141
+ if (config.storage) add(import_types39.EdgeType.DEPENDS_ON, "supabase-storage", "storage");
10142
+ if (config.auth) add(import_types39.EdgeType.DEPENDS_ON, "supabase-auth", "auth");
9860
10143
  }
9861
10144
  return { nodesAdded, edgesAdded };
9862
10145
  }
@@ -9884,11 +10167,11 @@ var import_node_path51 = __toESM(require("path"), 1);
9884
10167
  init_cjs_shims();
9885
10168
  var import_node_fs24 = require("fs");
9886
10169
  var import_node_path50 = __toESM(require("path"), 1);
9887
- var import_types39 = require("@neat.is/types");
10170
+ var import_types40 = require("@neat.is/types");
9888
10171
  function dropOrphanedFileNodes(graph) {
9889
10172
  const orphans = [];
9890
10173
  graph.forEachNode((id, attrs) => {
9891
- if (attrs.type !== import_types39.NodeType.FileNode) return;
10174
+ if (attrs.type !== import_types40.NodeType.FileNode) return;
9892
10175
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
9893
10176
  orphans.push(id);
9894
10177
  }
@@ -9901,7 +10184,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
9901
10184
  const bases = [scanPath, ...serviceDirs];
9902
10185
  graph.forEachEdge((id, attrs) => {
9903
10186
  const edge = attrs;
9904
- if (edge.provenance !== import_types39.Provenance.EXTRACTED) return;
10187
+ if (edge.provenance !== import_types40.Provenance.EXTRACTED) return;
9905
10188
  const evidenceFile = edge.evidence?.file;
9906
10189
  if (!evidenceFile) return;
9907
10190
  if (import_node_path50.default.isAbsolute(evidenceFile)) {
@@ -9932,6 +10215,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
9932
10215
  const routePhase = await addRoutes(graph, services);
9933
10216
  const grpcPhase = await addGrpcMethods(graph, services);
9934
10217
  const phase4 = await addCallEdges(graph, services);
10218
+ const tableEdges = await addTableEdges(graph, services);
9935
10219
  const phase5 = await addInfra(graph, scanPath, services);
9936
10220
  const ghostsRetired = retireExtractedEdgesByMissingFile(
9937
10221
  graph,
@@ -9971,8 +10255,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
9971
10255
  }
9972
10256
  }
9973
10257
  const result = {
9974
- nodesAdded: phase1Nodes + fileEnum.nodesAdded + symbolEnum.nodesAdded + importGraph.nodesAdded + symbolEdges.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
9975
- edgesAdded: fileEnum.edgesAdded + symbolEnum.edgesAdded + importGraph.edgesAdded + symbolEdges.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
10258
+ nodesAdded: phase1Nodes + fileEnum.nodesAdded + symbolEnum.nodesAdded + importGraph.nodesAdded + symbolEdges.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + tableEdges.nodesAdded + phase5.nodesAdded,
10259
+ edgesAdded: fileEnum.edgesAdded + symbolEnum.edgesAdded + importGraph.edgesAdded + symbolEdges.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + tableEdges.edgesAdded + phase5.edgesAdded,
9976
10260
  frontiersPromoted,
9977
10261
  extractionErrors: errorEntries.length,
9978
10262
  errorEntries,
@@ -9997,7 +10281,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
9997
10281
  init_cjs_shims();
9998
10282
  var import_node_fs25 = require("fs");
9999
10283
  var import_node_path52 = __toESM(require("path"), 1);
10000
- var import_types40 = require("@neat.is/types");
10284
+ var import_types41 = require("@neat.is/types");
10001
10285
  var SCHEMA_VERSION = 6;
10002
10286
  function migrateV1ToV2(payload) {
10003
10287
  const nodes = payload.graph.nodes;
@@ -10021,7 +10305,7 @@ function migrateV5ToV6(payload) {
10021
10305
  if (Array.isArray(nodes)) {
10022
10306
  for (const node of nodes) {
10023
10307
  const attrs = node.attributes;
10024
- if (!attrs || attrs.type !== import_types40.NodeType.InfraNode) continue;
10308
+ if (!attrs || attrs.type !== import_types41.NodeType.InfraNode) continue;
10025
10309
  if (attrs.kind !== "sql-table" && attrs.kind !== "supabase-table") continue;
10026
10310
  if (!Array.isArray(attrs.columns)) attrs.columns = [];
10027
10311
  }
@@ -10034,12 +10318,12 @@ function migrateV2ToV3(payload) {
10034
10318
  for (const edge of edges) {
10035
10319
  const attrs = edge.attributes;
10036
10320
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
10037
- attrs.provenance = import_types40.Provenance.OBSERVED;
10321
+ attrs.provenance = import_types41.Provenance.OBSERVED;
10038
10322
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
10039
10323
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
10040
10324
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
10041
10325
  if (type && source && target) {
10042
- const newId = (0, import_types40.observedEdgeId)(source, target, type);
10326
+ const newId = (0, import_types41.observedEdgeId)(source, target, type);
10043
10327
  attrs.id = newId;
10044
10328
  if (edge.key) edge.key = newId;
10045
10329
  }
@@ -10137,7 +10421,7 @@ function startPersistLoop(graph, outPath, opts = {}) {
10137
10421
  init_cjs_shims();
10138
10422
  var import_fastify2 = __toESM(require("fastify"), 1);
10139
10423
  var import_cors = __toESM(require("@fastify/cors"), 1);
10140
- var import_types59 = require("@neat.is/types");
10424
+ var import_types66 = require("@neat.is/types");
10141
10425
 
10142
10426
  // src/extend/index.ts
10143
10427
  init_cjs_shims();
@@ -10484,39 +10768,39 @@ async function rollbackExtension(ctx, args) {
10484
10768
 
10485
10769
  // src/divergences.ts
10486
10770
  init_cjs_shims();
10487
- var import_types41 = require("@neat.is/types");
10771
+ var import_types42 = require("@neat.is/types");
10488
10772
  function bucketKey(source, target, type) {
10489
10773
  return `${type}|${source}|${target}`;
10490
10774
  }
10491
10775
  function bucketSourceFor(graph, edge) {
10492
- if (edge.type !== import_types41.EdgeType.CONNECTS_TO) return edge.source;
10493
- const parsed = (0, import_types41.parseFileId)(edge.source);
10776
+ if (edge.type !== import_types42.EdgeType.CONNECTS_TO) return edge.source;
10777
+ const parsed = (0, import_types42.parseFileId)(edge.source);
10494
10778
  if (!parsed || !graph.hasNode(edge.target)) return edge.source;
10495
10779
  const target = graph.getNodeAttributes(edge.target);
10496
- if (target.type !== import_types41.NodeType.DatabaseNode) return edge.source;
10497
- return (0, import_types41.serviceId)(parsed.service);
10780
+ if (target.type !== import_types42.NodeType.DatabaseNode) return edge.source;
10781
+ return (0, import_types42.serviceId)(parsed.service);
10498
10782
  }
10499
10783
  function bucketEdges(graph) {
10500
10784
  const buckets2 = /* @__PURE__ */ new Map();
10501
10785
  graph.forEachEdge((id, attrs) => {
10502
10786
  const e = attrs;
10503
- const parsed = (0, import_types41.parseEdgeId)(id);
10787
+ const parsed = (0, import_types42.parseEdgeId)(id);
10504
10788
  const provenance = parsed?.provenance ?? e.provenance;
10505
10789
  const source = bucketSourceFor(graph, e);
10506
10790
  const key = bucketKey(source, e.target, e.type);
10507
10791
  const cur = buckets2.get(key) ?? { source, target: e.target, type: e.type };
10508
10792
  switch (provenance) {
10509
- case import_types41.Provenance.EXTRACTED:
10793
+ case import_types42.Provenance.EXTRACTED:
10510
10794
  cur.extracted = e;
10511
10795
  break;
10512
- case import_types41.Provenance.OBSERVED:
10796
+ case import_types42.Provenance.OBSERVED:
10513
10797
  cur.observed = e;
10514
10798
  break;
10515
- case import_types41.Provenance.INFERRED:
10799
+ case import_types42.Provenance.INFERRED:
10516
10800
  cur.inferred = e;
10517
10801
  break;
10518
10802
  default:
10519
- if (e.provenance === import_types41.Provenance.STALE) cur.stale = e;
10803
+ if (e.provenance === import_types42.Provenance.STALE) cur.stale = e;
10520
10804
  }
10521
10805
  buckets2.set(key, cur);
10522
10806
  });
@@ -10525,17 +10809,17 @@ function bucketEdges(graph) {
10525
10809
  function nodeIsFrontier(graph, nodeId) {
10526
10810
  if (!graph.hasNode(nodeId)) return false;
10527
10811
  const attrs = graph.getNodeAttributes(nodeId);
10528
- return attrs.type === import_types41.NodeType.FrontierNode;
10812
+ return attrs.type === import_types42.NodeType.FrontierNode;
10529
10813
  }
10530
10814
  function nodeIsWebsocketChannel(graph, nodeId) {
10531
10815
  if (!graph.hasNode(nodeId)) return false;
10532
10816
  const attrs = graph.getNodeAttributes(nodeId);
10533
- return attrs.type === import_types41.NodeType.WebSocketChannelNode;
10817
+ return attrs.type === import_types42.NodeType.WebSocketChannelNode;
10534
10818
  }
10535
10819
  function nodeIsSymbol(graph, nodeId) {
10536
10820
  if (!graph.hasNode(nodeId)) return false;
10537
10821
  const attrs = graph.getNodeAttributes(nodeId);
10538
- return attrs.type === import_types41.NodeType.SymbolNode;
10822
+ return attrs.type === import_types42.NodeType.SymbolNode;
10539
10823
  }
10540
10824
  function clampConfidence(n) {
10541
10825
  if (!Number.isFinite(n)) return 0;
@@ -10555,14 +10839,14 @@ function gradedConfidence(edge) {
10555
10839
  return clampConfidence(confidenceForEdge(edge));
10556
10840
  }
10557
10841
  var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
10558
- import_types41.EdgeType.CALLS,
10559
- import_types41.EdgeType.CONNECTS_TO,
10560
- import_types41.EdgeType.PUBLISHES_TO,
10561
- import_types41.EdgeType.CONSUMES_FROM
10842
+ import_types42.EdgeType.CALLS,
10843
+ import_types42.EdgeType.CONNECTS_TO,
10844
+ import_types42.EdgeType.PUBLISHES_TO,
10845
+ import_types42.EdgeType.CONSUMES_FROM
10562
10846
  ]);
10563
10847
  function detectMissingDivergences(graph, bucket) {
10564
10848
  const out = [];
10565
- if (bucket.type === import_types41.EdgeType.CONTAINS) return out;
10849
+ if (bucket.type === import_types42.EdgeType.CONTAINS) return out;
10566
10850
  if (nodeIsSymbol(graph, bucket.source) || nodeIsSymbol(graph, bucket.target)) return out;
10567
10851
  if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
10568
10852
  if (!nodeIsFrontier(graph, bucket.target)) {
@@ -10604,7 +10888,7 @@ function declaredHostFor(svc) {
10604
10888
  function hasExtractedConfiguredBy(graph, svcId) {
10605
10889
  for (const edgeId of graph.outboundEdges(svcId)) {
10606
10890
  const e = graph.getEdgeAttributes(edgeId);
10607
- if (e.type === import_types41.EdgeType.CONFIGURED_BY && e.provenance === import_types41.Provenance.EXTRACTED) {
10891
+ if (e.type === import_types42.EdgeType.CONFIGURED_BY && e.provenance === import_types42.Provenance.EXTRACTED) {
10608
10892
  return true;
10609
10893
  }
10610
10894
  }
@@ -10617,10 +10901,10 @@ function detectHostMismatch(graph, svcId, svc) {
10617
10901
  const out = [];
10618
10902
  for (const edgeId of graph.outboundEdges(svcId)) {
10619
10903
  const edge = graph.getEdgeAttributes(edgeId);
10620
- if (edge.type !== import_types41.EdgeType.CONNECTS_TO) continue;
10621
- if (edge.provenance !== import_types41.Provenance.OBSERVED) continue;
10904
+ if (edge.type !== import_types42.EdgeType.CONNECTS_TO) continue;
10905
+ if (edge.provenance !== import_types42.Provenance.OBSERVED) continue;
10622
10906
  const target = graph.getNodeAttributes(edge.target);
10623
- if (target.type !== import_types41.NodeType.DatabaseNode) continue;
10907
+ if (target.type !== import_types42.NodeType.DatabaseNode) continue;
10624
10908
  const observedHost = target.host?.trim();
10625
10909
  if (!observedHost) continue;
10626
10910
  if (observedHost === declaredHost) continue;
@@ -10642,10 +10926,10 @@ function detectCompatDivergences(graph, svcId, svc) {
10642
10926
  const deps = svc.dependencies ?? {};
10643
10927
  for (const edgeId of graph.outboundEdges(svcId)) {
10644
10928
  const edge = graph.getEdgeAttributes(edgeId);
10645
- if (edge.type !== import_types41.EdgeType.CONNECTS_TO) continue;
10646
- if (edge.provenance !== import_types41.Provenance.OBSERVED) continue;
10929
+ if (edge.type !== import_types42.EdgeType.CONNECTS_TO) continue;
10930
+ if (edge.provenance !== import_types42.Provenance.OBSERVED) continue;
10647
10931
  const target = graph.getNodeAttributes(edge.target);
10648
- if (target.type !== import_types41.NodeType.DatabaseNode) continue;
10932
+ if (target.type !== import_types42.NodeType.DatabaseNode) continue;
10649
10933
  for (const pair of compatPairs()) {
10650
10934
  if (pair.engine !== target.engine) continue;
10651
10935
  const declared = deps[pair.driver];
@@ -10742,7 +11026,7 @@ function suppressHostMismatchHalves(all) {
10742
11026
  for (const d of all) {
10743
11027
  if (d.type !== "host-mismatch") continue;
10744
11028
  observedHalf.add(`${d.source}->${d.target}`);
10745
- declaredHalf.add((0, import_types41.databaseId)(d.extractedHost));
11029
+ declaredHalf.add((0, import_types42.databaseId)(d.extractedHost));
10746
11030
  }
10747
11031
  if (observedHalf.size === 0) return all;
10748
11032
  return all.filter((d) => {
@@ -10761,13 +11045,13 @@ function computeDivergences(graph, opts = {}) {
10761
11045
  }
10762
11046
  graph.forEachNode((nodeId, attrs) => {
10763
11047
  const n = attrs;
10764
- if (n.type === import_types41.NodeType.ServiceNode) {
11048
+ if (n.type === import_types42.NodeType.ServiceNode) {
10765
11049
  const svc = n;
10766
11050
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
10767
11051
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
10768
11052
  return;
10769
11053
  }
10770
- if (n.type === import_types41.NodeType.InfraNode && n.kind === "sql-table") {
11054
+ if (n.type === import_types42.NodeType.InfraNode && n.kind === "sql-table") {
10771
11055
  for (const d of detectColumnDrift(n)) all.push(d);
10772
11056
  }
10773
11057
  });
@@ -10803,7 +11087,7 @@ function computeDivergences(graph, opts = {}) {
10803
11087
  const bc = "column" in b && b.column ? b.column : "";
10804
11088
  return ac.localeCompare(bc);
10805
11089
  });
10806
- return import_types41.DivergenceResultSchema.parse({
11090
+ return import_types42.DivergenceResultSchema.parse({
10807
11091
  divergences: filtered,
10808
11092
  totalAffected: filtered.length,
10809
11093
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -10989,7 +11273,7 @@ init_cjs_shims();
10989
11273
  var import_node_fs29 = require("fs");
10990
11274
  var import_node_os3 = __toESM(require("os"), 1);
10991
11275
  var import_node_path56 = __toESM(require("path"), 1);
10992
- var import_types42 = require("@neat.is/types");
11276
+ var import_types43 = require("@neat.is/types");
10993
11277
  var LOCK_TIMEOUT_MS = 5e3;
10994
11278
  var LOCK_RETRY_MS = 50;
10995
11279
  function neatHome() {
@@ -11199,10 +11483,10 @@ async function readRegistry() {
11199
11483
  throw err;
11200
11484
  }
11201
11485
  const parsed = JSON.parse(raw);
11202
- return import_types42.RegistryFileSchema.parse(parsed);
11486
+ return import_types43.RegistryFileSchema.parse(parsed);
11203
11487
  }
11204
11488
  async function writeRegistry(reg) {
11205
- const validated = import_types42.RegistryFileSchema.parse(reg);
11489
+ const validated = import_types43.RegistryFileSchema.parse(reg);
11206
11490
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
11207
11491
  }
11208
11492
  var ProjectNameCollisionError = class extends Error {
@@ -11603,15 +11887,15 @@ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_
11603
11887
 
11604
11888
  // src/connectors/index.ts
11605
11889
  init_cjs_shims();
11606
- var import_types43 = require("@neat.is/types");
11890
+ var import_types44 = require("@neat.is/types");
11607
11891
  var NO_ENV = "unknown";
11608
11892
  function staticCallSiteFor(graph, serviceName, targetNodeId) {
11609
11893
  if (!graph.hasNode(targetNodeId)) return void 0;
11610
11894
  const sites = [];
11611
11895
  for (const edgeId of graph.inboundEdges(targetNodeId)) {
11612
11896
  const edge = graph.getEdgeAttributes(edgeId);
11613
- if (edge.provenance !== import_types43.Provenance.EXTRACTED) continue;
11614
- const parsed = (0, import_types43.parseFileId)(edge.source);
11897
+ if (edge.provenance !== import_types44.Provenance.EXTRACTED) continue;
11898
+ const parsed = (0, import_types44.parseFileId)(edge.source);
11615
11899
  if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
11616
11900
  const site = { relPath: edge.evidence.file };
11617
11901
  if (edge.evidence.line !== void 0) site.line = edge.evidence.line;
@@ -11622,7 +11906,7 @@ function staticCallSiteFor(graph, serviceName, targetNodeId) {
11622
11906
  function routeCallSiteFor(graph, targetNodeId) {
11623
11907
  if (!graph.hasNode(targetNodeId)) return void 0;
11624
11908
  const attrs = graph.getNodeAttributes(targetNodeId);
11625
- if (attrs.type !== import_types43.NodeType.RouteNode || !attrs.path) return void 0;
11909
+ if (attrs.type !== import_types44.NodeType.RouteNode || !attrs.path) return void 0;
11626
11910
  const site = { relPath: attrs.path };
11627
11911
  if (attrs.line !== void 0) site.line = attrs.line;
11628
11912
  return site;
@@ -11806,7 +12090,11 @@ var JUNCTION_DEFAULT_RATE_LIMITS = {
11806
12090
  // connector add/remove/test` (provision/deprovision/validate), never a poll
11807
12091
  // loop, so this bucket is exercised a handful of times per command. Kept
11808
12092
  // conservative pending a documented Drains-API rate limit.
11809
- vercel: { capacity: 20, refillMs: 5e3 }
12093
+ vercel: { capacity: 20, refillMs: 5e3 },
12094
+ // Render's REST API (api-docs.render.com/reference/rate-limiting) isn't
12095
+ // pinned here to a single confirmed number — this is a conservative
12096
+ // placeholder pending a live project, matching the other pull providers.
12097
+ render: { capacity: 30, refillMs: 1e4 }
11810
12098
  };
11811
12099
  var JUNCTION_GENERIC_RATE_LIMIT = { capacity: 20, refillMs: 5e3 };
11812
12100
  function defaultRateLimitFor(provider) {
@@ -12213,23 +12501,23 @@ async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT
12213
12501
 
12214
12502
  // src/connectors/supabase/resolve.ts
12215
12503
  init_cjs_shims();
12216
- var import_types45 = require("@neat.is/types");
12504
+ var import_types46 = require("@neat.is/types");
12217
12505
  function createSupabaseResolveTarget(graph, config) {
12218
12506
  return (signal, _ctx) => {
12219
12507
  if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
12220
12508
  return null;
12221
12509
  }
12222
- const subResourceId = (0, import_types45.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
12510
+ const subResourceId = (0, import_types46.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
12223
12511
  if (graph.hasNode(subResourceId)) {
12224
- return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types45.EdgeType.CALLS };
12512
+ return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types46.EdgeType.CALLS };
12225
12513
  }
12226
- const bareResourceId = (0, import_types45.infraId)(signal.targetKind, signal.targetName);
12514
+ const bareResourceId = (0, import_types46.infraId)(signal.targetKind, signal.targetName);
12227
12515
  if (graph.hasNode(bareResourceId)) {
12228
- return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: import_types45.EdgeType.CALLS };
12516
+ return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: import_types46.EdgeType.CALLS };
12229
12517
  }
12230
- const projectLevelId = (0, import_types45.infraId)("supabase", config.nodeRef);
12518
+ const projectLevelId = (0, import_types46.infraId)("supabase", config.nodeRef);
12231
12519
  if (graph.hasNode(projectLevelId)) {
12232
- return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types45.EdgeType.CALLS };
12520
+ return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types46.EdgeType.CALLS };
12233
12521
  }
12234
12522
  return null;
12235
12523
  };
@@ -12322,7 +12610,7 @@ function createSupabaseConnector(graph, config, deps = {}) {
12322
12610
 
12323
12611
  // src/connectors/railway/index.ts
12324
12612
  init_cjs_shims();
12325
- var import_types49 = require("@neat.is/types");
12613
+ var import_types50 = require("@neat.is/types");
12326
12614
 
12327
12615
  // src/connectors/railway/client.ts
12328
12616
  init_cjs_shims();
@@ -12473,7 +12761,7 @@ function buildRailwayRouteIndex(graph, serviceName) {
12473
12761
  const out = [];
12474
12762
  graph.forEachNode((_id, attrs) => {
12475
12763
  const node = attrs;
12476
- if (node.type !== import_types49.NodeType.RouteNode) return;
12764
+ if (node.type !== import_types50.NodeType.RouteNode) return;
12477
12765
  const route = attrs;
12478
12766
  if (route.service !== serviceName) return;
12479
12767
  out.push({
@@ -12577,12 +12865,12 @@ function createRailwayResolveTarget(config) {
12577
12865
  const serviceName = config.serviceNameById[config.serviceId];
12578
12866
  if (!serviceName) return null;
12579
12867
  if (signal.targetKind === ROUTE_TARGET_KIND) {
12580
- return { targetNodeId: signal.targetName, serviceName, edgeType: import_types49.EdgeType.CALLS };
12868
+ return { targetNodeId: signal.targetName, serviceName, edgeType: import_types50.EdgeType.CALLS };
12581
12869
  }
12582
12870
  if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
12583
12871
  const peerName = config.serviceNameById[signal.targetName];
12584
12872
  if (!peerName) return null;
12585
- return { targetNodeId: (0, import_types49.serviceId)(peerName), serviceName, edgeType: import_types49.EdgeType.CONNECTS_TO };
12873
+ return { targetNodeId: (0, import_types50.serviceId)(peerName), serviceName, edgeType: import_types50.EdgeType.CONNECTS_TO };
12586
12874
  }
12587
12875
  return null;
12588
12876
  };
@@ -12770,7 +13058,7 @@ function mapLogEntriesToSignals(entries) {
12770
13058
 
12771
13059
  // src/connectors/firebase/resolve.ts
12772
13060
  init_cjs_shims();
12773
- var import_types50 = require("@neat.is/types");
13061
+ var import_types51 = require("@neat.is/types");
12774
13062
  function neatServiceNameFor(resourceType, resourceName, serviceMap) {
12775
13063
  switch (resourceType) {
12776
13064
  case "cloud_function":
@@ -12785,7 +13073,7 @@ function routeEntriesFor(graph, serviceName) {
12785
13073
  const entries = [];
12786
13074
  graph.forEachNode((_id, attrs) => {
12787
13075
  const node = attrs;
12788
- if (node.type !== import_types50.NodeType.RouteNode) return;
13076
+ if (node.type !== import_types51.NodeType.RouteNode) return;
12789
13077
  const route = attrs;
12790
13078
  if (route.service !== serviceName) return;
12791
13079
  entries.push({
@@ -12817,7 +13105,7 @@ function createFirebaseResolveTarget(graph, serviceMap) {
12817
13105
  return {
12818
13106
  targetNodeId: match.routeNodeId,
12819
13107
  serviceName,
12820
- edgeType: import_types50.EdgeType.CALLS
13108
+ edgeType: import_types51.EdgeType.CALLS
12821
13109
  };
12822
13110
  };
12823
13111
  }
@@ -12844,7 +13132,7 @@ init_cjs_shims();
12844
13132
 
12845
13133
  // src/connectors/cloudflare/connector.ts
12846
13134
  init_cjs_shims();
12847
- var import_types52 = require("@neat.is/types");
13135
+ var import_types53 = require("@neat.is/types");
12848
13136
 
12849
13137
  // src/connectors/cloudflare/client.ts
12850
13138
  init_cjs_shims();
@@ -13008,7 +13296,7 @@ function findTaggedWorkerFileNode(graph, workerName) {
13008
13296
  graph.forEachNode((id, attrs) => {
13009
13297
  if (found) return;
13010
13298
  const a = attrs;
13011
- if (a.type === import_types52.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
13299
+ if (a.type === import_types53.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
13012
13300
  found = id;
13013
13301
  }
13014
13302
  });
@@ -13020,7 +13308,7 @@ function findMatchingRouteNode(graph, serviceName, method, path60) {
13020
13308
  graph.forEachNode((id, attrs) => {
13021
13309
  if (found) return;
13022
13310
  const a = attrs;
13023
- if (a.type !== import_types52.NodeType.RouteNode || a.service !== serviceName) return;
13311
+ if (a.type !== import_types53.NodeType.RouteNode || a.service !== serviceName) return;
13024
13312
  if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
13025
13313
  const routeMethod = (a.method ?? "").toUpperCase();
13026
13314
  if (routeMethod !== "ALL" && routeMethod !== method) return;
@@ -13039,11 +13327,11 @@ function createCloudflareResolveTarget(config, graph) {
13039
13327
  };
13040
13328
  const mapping = config.workers?.[scriptName];
13041
13329
  if (mapping) {
13042
- const wholeFileId = (0, import_types52.fileId)(mapping.service, mapping.entryFile);
13330
+ const wholeFileId = (0, import_types53.fileId)(mapping.service, mapping.entryFile);
13043
13331
  return {
13044
13332
  targetNodeId: resolveRouteGrain(mapping.service, wholeFileId),
13045
13333
  serviceName: mapping.service,
13046
- edgeType: import_types52.EdgeType.CALLS
13334
+ edgeType: import_types53.EdgeType.CALLS
13047
13335
  };
13048
13336
  }
13049
13337
  const taggedFileId = findTaggedWorkerFileNode(graph, scriptName);
@@ -13052,13 +13340,13 @@ function createCloudflareResolveTarget(config, graph) {
13052
13340
  return {
13053
13341
  targetNodeId: resolveRouteGrain(fileNode.service, taggedFileId),
13054
13342
  serviceName: fileNode.service,
13055
- edgeType: import_types52.EdgeType.CALLS
13343
+ edgeType: import_types53.EdgeType.CALLS
13056
13344
  };
13057
13345
  }
13058
13346
  return {
13059
- targetNodeId: (0, import_types52.infraId)("cloudflare-worker", scriptName),
13347
+ targetNodeId: (0, import_types53.infraId)("cloudflare-worker", scriptName),
13060
13348
  serviceName: scriptName,
13061
- edgeType: import_types52.EdgeType.CALLS,
13349
+ edgeType: import_types53.EdgeType.CALLS,
13062
13350
  ensureInfraNode: { kind: "cloudflare-worker", name: scriptName, provider: "cloudflare" }
13063
13351
  };
13064
13352
  };
@@ -13254,14 +13542,14 @@ function diffNeonStatementsToSignals(rows, previous, observedAtIso) {
13254
13542
 
13255
13543
  // src/connectors/neon/resolve.ts
13256
13544
  init_cjs_shims();
13257
- var import_types56 = require("@neat.is/types");
13545
+ var import_types57 = require("@neat.is/types");
13258
13546
  function createNeonResolveTarget(config) {
13259
13547
  return (signal) => {
13260
13548
  if (signal.targetKind !== NEON_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
13261
13549
  return {
13262
- targetNodeId: (0, import_types56.infraId)("sql-table", signal.targetName),
13550
+ targetNodeId: (0, import_types57.infraId)("sql-table", signal.targetName),
13263
13551
  serviceName: config.serviceName,
13264
- edgeType: import_types56.EdgeType.CALLS,
13552
+ edgeType: import_types57.EdgeType.CALLS,
13265
13553
  ensureInfraNode: { kind: "sql-table", name: signal.targetName, provider: "neon" }
13266
13554
  };
13267
13555
  };
@@ -13299,6 +13587,412 @@ function createNeonConnector(config, deps = {}) {
13299
13587
  };
13300
13588
  }
13301
13589
 
13590
+ // src/connectors/cloud-run/index.ts
13591
+ init_cjs_shims();
13592
+
13593
+ // src/connectors/cloud-run/client.ts
13594
+ init_cjs_shims();
13595
+ function cloudRunRequestLogName(projectId) {
13596
+ return `projects/${projectId}/logs/run.googleapis.com%2Frequests`;
13597
+ }
13598
+ function buildCloudRunEntriesFilter(projectId, sinceIso) {
13599
+ return [
13600
+ `logName = "${cloudRunRequestLogName(projectId)}"`,
13601
+ `resource.type = "${CLOUD_RUN_RESOURCE_TYPE}"`,
13602
+ 'httpRequest.requestMethod != ""',
13603
+ `timestamp >= "${sinceIso}"`
13604
+ ].join(" AND ");
13605
+ }
13606
+ var CLOUD_RUN_RESOURCE_TYPE = "cloud_run_revision";
13607
+ var DEFAULT_LOOKBACK_MS2 = 24 * 60 * 60 * 1e3;
13608
+ var ENTRIES_LIST_URL2 = "https://logging.googleapis.com/v2/entries:list";
13609
+ var PAGE_SIZE2 = 1e3;
13610
+ var MAX_PAGES2 = 20;
13611
+ async function fetchCloudRunRequestLogEntries(creds, sinceIso, apiUrl = ENTRIES_LIST_URL2) {
13612
+ const filter = buildCloudRunEntriesFilter(creds.projectId, sinceIso);
13613
+ const out = [];
13614
+ let pageToken;
13615
+ for (let page = 0; page < MAX_PAGES2; page++) {
13616
+ const body = {
13617
+ resourceNames: [`projects/${creds.projectId}`],
13618
+ filter,
13619
+ orderBy: "timestamp asc",
13620
+ pageSize: PAGE_SIZE2,
13621
+ ...pageToken ? { pageToken } : {}
13622
+ };
13623
+ const res = await junctionFetch(
13624
+ apiUrl,
13625
+ {
13626
+ method: "POST",
13627
+ headers: {
13628
+ ...bearerAuthHeader(creds.accessToken),
13629
+ "Content-Type": "application/json"
13630
+ },
13631
+ body: JSON.stringify(body)
13632
+ },
13633
+ // accountKey: the GCP project id — one customer's Cloud Logging quota is
13634
+ // scoped per GCP project (ADR-131's per-(provider, accountKey) rate-limit
13635
+ // bucket), the same key Firebase's connector uses.
13636
+ { provider: "cloud-run", accountKey: creds.projectId }
13637
+ );
13638
+ if (!res.ok) {
13639
+ throw new Error(`Cloud Logging entries.list failed: ${res.status} ${res.statusText}`);
13640
+ }
13641
+ const json = await res.json();
13642
+ if (Array.isArray(json.entries)) out.push(...json.entries);
13643
+ if (!json.nextPageToken) break;
13644
+ pageToken = json.nextPageToken;
13645
+ }
13646
+ return out;
13647
+ }
13648
+
13649
+ // src/connectors/cloud-run/map.ts
13650
+ init_cjs_shims();
13651
+
13652
+ // src/connectors/cloud-run/types.ts
13653
+ init_cjs_shims();
13654
+ function readCloudRunCredentials(raw) {
13655
+ const projectId = raw["projectId"];
13656
+ const accessToken = raw["accessToken"];
13657
+ if (typeof projectId !== "string" || projectId.length === 0) {
13658
+ throw new Error("cloud-run connector: credentials.projectId must be a non-empty string");
13659
+ }
13660
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
13661
+ throw new Error("cloud-run connector: credentials.accessToken must be a non-empty string");
13662
+ }
13663
+ return { projectId, accessToken };
13664
+ }
13665
+ var CLOUD_RUN_TARGET_KIND = "cloud_run_revision";
13666
+ var FIELD_SEP2 = "\0";
13667
+ function packCloudRunTargetName(identity) {
13668
+ return [identity.serviceName, identity.method, identity.path].join(FIELD_SEP2);
13669
+ }
13670
+ function parseCloudRunTargetName(targetName) {
13671
+ const firstSep = targetName.indexOf(FIELD_SEP2);
13672
+ if (firstSep === -1) return null;
13673
+ const serviceName = targetName.slice(0, firstSep);
13674
+ const rest = targetName.slice(firstSep + 1);
13675
+ const secondSep = rest.indexOf(FIELD_SEP2);
13676
+ if (secondSep === -1) return null;
13677
+ const method = rest.slice(0, secondSep);
13678
+ const path60 = rest.slice(secondSep + 1);
13679
+ if (!serviceName || !method || !path60) return null;
13680
+ return { serviceName, method, path: path60 };
13681
+ }
13682
+
13683
+ // src/connectors/cloud-run/map.ts
13684
+ var CLOUD_RUN_RESOURCE_TYPE2 = "cloud_run_revision";
13685
+ function pathFromRequestUrl2(requestUrl) {
13686
+ if (typeof requestUrl !== "string" || requestUrl.length === 0) return null;
13687
+ if (requestUrl.startsWith("/")) {
13688
+ const withoutQuery = requestUrl.split("?")[0];
13689
+ return withoutQuery && withoutQuery.length > 0 ? withoutQuery : "/";
13690
+ }
13691
+ try {
13692
+ const candidate = requestUrl.startsWith("//") ? `https:${requestUrl}` : requestUrl;
13693
+ const parsed = new URL(candidate);
13694
+ return parsed.pathname || "/";
13695
+ } catch {
13696
+ return null;
13697
+ }
13698
+ }
13699
+ var ERROR_STATUS_THRESHOLD4 = 500;
13700
+ function mapLogEntryToSignal2(entry) {
13701
+ if (!entry || typeof entry !== "object") return null;
13702
+ if (entry.resource?.type !== CLOUD_RUN_RESOURCE_TYPE2) return null;
13703
+ const serviceName = entry.resource?.labels?.["service_name"];
13704
+ if (typeof serviceName !== "string" || serviceName.length === 0) return null;
13705
+ const req = entry.httpRequest;
13706
+ if (!req) return null;
13707
+ if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
13708
+ const method = req.requestMethod.toUpperCase();
13709
+ const path60 = pathFromRequestUrl2(req.requestUrl);
13710
+ if (path60 === null) return null;
13711
+ const timestamp = entry.timestamp;
13712
+ if (typeof timestamp !== "string" || timestamp.length === 0) return null;
13713
+ const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD4;
13714
+ return {
13715
+ targetKind: CLOUD_RUN_TARGET_KIND,
13716
+ targetName: packCloudRunTargetName({ serviceName, method, path: path60 }),
13717
+ callCount: 1,
13718
+ errorCount: isError ? 1 : 0,
13719
+ lastObservedIso: timestamp
13720
+ };
13721
+ }
13722
+ function mapLogEntriesToSignals2(entries) {
13723
+ const out = [];
13724
+ for (const entry of entries) {
13725
+ const signal = mapLogEntryToSignal2(entry);
13726
+ if (signal) out.push(signal);
13727
+ }
13728
+ return out;
13729
+ }
13730
+
13731
+ // src/connectors/cloud-run/resolve.ts
13732
+ init_cjs_shims();
13733
+ var import_types61 = require("@neat.is/types");
13734
+ var CLOUD_RUN_SERVICE_INFRA_KIND = "cloud-run-service";
13735
+ function findMatchingRouteNode2(graph, serviceName, method, normalizedPath) {
13736
+ let found = null;
13737
+ graph.forEachNode((_id, attrs) => {
13738
+ if (found) return;
13739
+ const node = attrs;
13740
+ if (node.type !== import_types61.NodeType.RouteNode) return;
13741
+ const route = attrs;
13742
+ if (route.service !== serviceName || !route.pathTemplate) return;
13743
+ if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
13744
+ const routeMethod = route.method.toUpperCase();
13745
+ if (routeMethod !== "ALL" && routeMethod !== method) return;
13746
+ found = route.id;
13747
+ });
13748
+ return found;
13749
+ }
13750
+ function createCloudRunResolveTarget(graph, config) {
13751
+ return (signal) => {
13752
+ if (signal.targetKind !== CLOUD_RUN_TARGET_KIND) return null;
13753
+ const identity = parseCloudRunTargetName(signal.targetName);
13754
+ if (!identity) return null;
13755
+ const { serviceName: gcpServiceName, method, path: path60 } = identity;
13756
+ const mappedService = config.serviceMap?.[gcpServiceName];
13757
+ if (mappedService) {
13758
+ const routeNodeId = findMatchingRouteNode2(
13759
+ graph,
13760
+ mappedService,
13761
+ method,
13762
+ normalizePathTemplate(path60)
13763
+ );
13764
+ if (routeNodeId) {
13765
+ return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types61.EdgeType.CALLS };
13766
+ }
13767
+ }
13768
+ return {
13769
+ targetNodeId: (0, import_types61.infraId)(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
13770
+ serviceName: mappedService ?? gcpServiceName,
13771
+ edgeType: import_types61.EdgeType.CALLS,
13772
+ ensureInfraNode: {
13773
+ kind: CLOUD_RUN_SERVICE_INFRA_KIND,
13774
+ name: gcpServiceName,
13775
+ provider: "cloud-run"
13776
+ }
13777
+ };
13778
+ };
13779
+ }
13780
+
13781
+ // src/connectors/cloud-run/index.ts
13782
+ var CloudRunConnector = class {
13783
+ constructor(config = {}) {
13784
+ this.config = config;
13785
+ }
13786
+ config;
13787
+ provider = "cloud-run";
13788
+ async poll(ctx) {
13789
+ const creds = readCloudRunCredentials(ctx.credentials);
13790
+ const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_LOOKBACK_MS2;
13791
+ const sinceIso = boundedSinceIso(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
13792
+ const entries = await fetchCloudRunRequestLogEntries(creds, sinceIso, this.config.apiUrl);
13793
+ return mapLogEntriesToSignals2(entries);
13794
+ }
13795
+ };
13796
+ function boundedSinceIso(since, now, maxLookbackMs) {
13797
+ const floor = new Date(now.getTime() - maxLookbackMs);
13798
+ if (!since) return floor.toISOString();
13799
+ const sinceMs = new Date(since).getTime();
13800
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
13801
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
13802
+ }
13803
+ function createCloudRunConnector(graph, config = {}) {
13804
+ return {
13805
+ connector: new CloudRunConnector(config),
13806
+ resolveTarget: createCloudRunResolveTarget(graph, config)
13807
+ };
13808
+ }
13809
+
13810
+ // src/connectors/render/index.ts
13811
+ init_cjs_shims();
13812
+ var import_types64 = require("@neat.is/types");
13813
+
13814
+ // src/connectors/render/types.ts
13815
+ init_cjs_shims();
13816
+ function readRenderToken(credentials) {
13817
+ const token = credentials.token;
13818
+ if (typeof token !== "string" || token.length === 0) {
13819
+ throw new Error("Render connector requires ctx.credentials.token (a Render API key)");
13820
+ }
13821
+ return token;
13822
+ }
13823
+ function renderLabelValue(entry, name) {
13824
+ if (!Array.isArray(entry.labels)) return void 0;
13825
+ const label = entry.labels.find((l) => l && typeof l === "object" && l.name === name);
13826
+ return label && typeof label.value === "string" ? label.value : void 0;
13827
+ }
13828
+
13829
+ // src/connectors/render/client.ts
13830
+ init_cjs_shims();
13831
+ var DEFAULT_RENDER_API_URL = "https://api.render.com/v1";
13832
+ var DEFAULT_RENDER_LOG_LIMIT = 100;
13833
+ var RENDER_MAX_LOG_LIMIT = 100;
13834
+ var DEFAULT_RENDER_MAX_PAGES = 20;
13835
+ var DEFAULT_MAX_LOOKBACK_MS4 = 24 * 60 * 60 * 1e3;
13836
+ function clampLimit(limit) {
13837
+ const raw = Math.trunc(limit ?? DEFAULT_RENDER_LOG_LIMIT);
13838
+ if (!Number.isFinite(raw) || raw < 1) return DEFAULT_RENDER_LOG_LIMIT;
13839
+ return Math.min(raw, RENDER_MAX_LOG_LIMIT);
13840
+ }
13841
+ function boundedRenderStartTime(since, now, maxLookbackMs) {
13842
+ const floor = new Date(now.getTime() - maxLookbackMs);
13843
+ if (!since) return floor.toISOString();
13844
+ const sinceMs = new Date(since).getTime();
13845
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
13846
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
13847
+ }
13848
+ async function fetchRenderLogPage(config, token, startTime, endTime, limit, fetchImpl) {
13849
+ const url = new URL(`${config.apiUrl ?? DEFAULT_RENDER_API_URL}/logs`);
13850
+ url.searchParams.set("ownerId", config.ownerId);
13851
+ url.searchParams.set("resource", config.resourceId);
13852
+ url.searchParams.set("type", "request");
13853
+ url.searchParams.set("startTime", startTime);
13854
+ url.searchParams.set("endTime", endTime);
13855
+ url.searchParams.set("direction", "backward");
13856
+ url.searchParams.set("limit", String(limit));
13857
+ const res = await junctionFetch(
13858
+ url,
13859
+ { method: "GET", headers: { ...bearerAuthHeader(token) } },
13860
+ { provider: "render", accountKey: config.ownerId, ...fetchImpl ? { fetchImpl } : {} }
13861
+ );
13862
+ if (!res.ok) {
13863
+ throw new Error(`Render logs request failed: ${res.status} ${res.statusText}`);
13864
+ }
13865
+ return await res.json();
13866
+ }
13867
+ async function fetchRenderRequestLogs(config, token, startTime, endTime, fetchImpl) {
13868
+ const limit = clampLimit(config.limit);
13869
+ const maxPages = Math.max(1, Math.trunc(config.maxPages ?? DEFAULT_RENDER_MAX_PAGES));
13870
+ const out = [];
13871
+ let pageStart = startTime;
13872
+ let pageEnd = endTime;
13873
+ for (let page = 0; page < maxPages; page++) {
13874
+ const body = await fetchRenderLogPage(config, token, pageStart, pageEnd, limit, fetchImpl);
13875
+ if (Array.isArray(body.logs)) out.push(...body.logs);
13876
+ if (!body.hasMore || !body.nextStartTime || !body.nextEndTime) break;
13877
+ pageStart = body.nextStartTime;
13878
+ pageEnd = body.nextEndTime;
13879
+ }
13880
+ return out;
13881
+ }
13882
+
13883
+ // src/connectors/render/index.ts
13884
+ var ROUTE_TARGET_KIND2 = "route";
13885
+ var UNMATCHED_ROUTE_TARGET_KIND2 = "unmatched-route";
13886
+ function buildRenderRouteIndex(graph, serviceName) {
13887
+ const out = [];
13888
+ graph.forEachNode((_id, attrs) => {
13889
+ const node = attrs;
13890
+ if (node.type !== import_types64.NodeType.RouteNode) return;
13891
+ const route = attrs;
13892
+ if (route.service !== serviceName) return;
13893
+ out.push({
13894
+ method: route.method.toUpperCase(),
13895
+ normalizedPath: normalizePathTemplate(route.pathTemplate),
13896
+ routeNodeId: route.id,
13897
+ path: route.path,
13898
+ line: route.line
13899
+ });
13900
+ });
13901
+ return out;
13902
+ }
13903
+ function findRenderRoute(entries, method, normalizedPath) {
13904
+ return entries.find(
13905
+ (e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || e.method === method)
13906
+ );
13907
+ }
13908
+ function bucketKey3(method, normalizedPath) {
13909
+ return `${method} ${normalizedPath}`;
13910
+ }
13911
+ function isHttpErrorStatus2(status2) {
13912
+ return status2 >= 400;
13913
+ }
13914
+ function upsertBucket2(buckets2, key, isError, timestamp, build) {
13915
+ const existing = buckets2.get(key);
13916
+ if (existing) {
13917
+ existing.callCount += 1;
13918
+ if (isError) existing.errorCount += 1;
13919
+ if (timestamp > existing.lastObservedIso) existing.lastObservedIso = timestamp;
13920
+ return;
13921
+ }
13922
+ buckets2.set(key, { callCount: 1, errorCount: isError ? 1 : 0, lastObservedIso: timestamp, ...build() });
13923
+ }
13924
+ function mapRenderRequestLogsToSignals(entries, routeIndex) {
13925
+ const buckets2 = /* @__PURE__ */ new Map();
13926
+ if (!Array.isArray(entries)) return [];
13927
+ for (const entry of entries) {
13928
+ if (!entry || typeof entry !== "object") continue;
13929
+ if (typeof entry.timestamp !== "string") continue;
13930
+ const method = renderLabelValue(entry, "method");
13931
+ const rawPath = renderLabelValue(entry, "path");
13932
+ if (typeof method !== "string" || method.length === 0) continue;
13933
+ if (typeof rawPath !== "string" || rawPath.length === 0) continue;
13934
+ const methodUpper = method.toUpperCase();
13935
+ const pathOnly = rawPath.split("?")[0];
13936
+ const normalizedPath = normalizePathTemplate(pathOnly);
13937
+ const statusCode = Number.parseInt(renderLabelValue(entry, "statusCode") ?? "", 10);
13938
+ const isError = Number.isFinite(statusCode) && isHttpErrorStatus2(statusCode);
13939
+ const match = findRenderRoute(routeIndex, methodUpper, normalizedPath);
13940
+ if (match) {
13941
+ upsertBucket2(buckets2, `route:${match.routeNodeId}`, isError, entry.timestamp, () => ({
13942
+ targetKind: ROUTE_TARGET_KIND2,
13943
+ targetName: match.routeNodeId,
13944
+ // RouteNode.line is optional in the schema (packages/types/src/
13945
+ // nodes.ts) even though routes.ts always sets it today — skip the
13946
+ // callSite rather than fabricate a line when it's ever absent
13947
+ // (file-awareness.md §6).
13948
+ ...match.line !== void 0 ? { callSite: { file: match.path, line: match.line } } : {}
13949
+ }));
13950
+ } else {
13951
+ upsertBucket2(
13952
+ buckets2,
13953
+ `unmatched:${bucketKey3(methodUpper, normalizedPath)}`,
13954
+ isError,
13955
+ entry.timestamp,
13956
+ () => ({
13957
+ targetKind: UNMATCHED_ROUTE_TARGET_KIND2,
13958
+ targetName: bucketKey3(methodUpper, normalizedPath)
13959
+ })
13960
+ );
13961
+ }
13962
+ }
13963
+ return [...buckets2.values()].map((b) => ({
13964
+ targetKind: b.targetKind,
13965
+ targetName: b.targetName,
13966
+ callCount: b.callCount,
13967
+ errorCount: b.errorCount,
13968
+ lastObservedIso: b.lastObservedIso,
13969
+ ...b.callSite ? { callSite: b.callSite } : {}
13970
+ }));
13971
+ }
13972
+ function createRenderResolveTarget(config) {
13973
+ return (signal) => {
13974
+ if (signal.targetKind === ROUTE_TARGET_KIND2) {
13975
+ return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types64.EdgeType.CALLS };
13976
+ }
13977
+ return null;
13978
+ };
13979
+ }
13980
+ function createRenderConnector(graph, config) {
13981
+ return {
13982
+ provider: "render",
13983
+ async poll(ctx) {
13984
+ const token = readRenderToken(ctx.credentials);
13985
+ const now = /* @__PURE__ */ new Date();
13986
+ const maxLookbackMs = config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS4;
13987
+ const startTime = boundedRenderStartTime(ctx.since, now, maxLookbackMs);
13988
+ const endTime = now.toISOString();
13989
+ const logs = await fetchRenderRequestLogs(config, token, startTime, endTime);
13990
+ const routeIndex = buildRenderRouteIndex(graph, config.serviceName);
13991
+ return mapRenderRequestLogsToSignals(logs, routeIndex);
13992
+ }
13993
+ };
13994
+ }
13995
+
13302
13996
  // src/connectors/registry.ts
13303
13997
  var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
13304
13998
  async function authProbe(input) {
@@ -13474,6 +14168,71 @@ var PROVIDER_DISPATCH = {
13474
14168
  return { ok: false, reason: `neon telemetry read failed: ${err.message}` };
13475
14169
  }
13476
14170
  }
14171
+ },
14172
+ "cloud-run": {
14173
+ provider: "cloud-run",
14174
+ // Cloud Run reads both projectId and accessToken from the credential; the
14175
+ // single-string form maps to the secret (the token), and the required-fields
14176
+ // check below catches a projectId that was never supplied.
14177
+ primaryCredentialKey: "accessToken",
14178
+ requiredCredentialFields: ["projectId", "accessToken"],
14179
+ requiredOptionFields: [],
14180
+ build(graph, options) {
14181
+ return createCloudRunConnector(graph, options);
14182
+ },
14183
+ // POST entries:list with pageSize 1 — the exact surface poll() reads, so the
14184
+ // probe checks the actual `logging.logEntries.list` permission the connector
14185
+ // needs. A GET on the lighter logs.list endpoint (as Firebase probes) would
14186
+ // instead check `logging.logs.list`, falsely rejecting a correctly-scoped
14187
+ // custom role that carries only `logging.logEntries.list` (the narrowest
14188
+ // grant docs/connectors/cloud-run.md documents) — the same false-negative
14189
+ // trap Railway's validate avoids by probing its real query. A 2xx means the
14190
+ // token can list log entries; 401/403 means the provider rejected it.
14191
+ validate({ credentials, fetchImpl }) {
14192
+ const projectId = String(credentials.projectId ?? "");
14193
+ return authProbe({
14194
+ provider: "cloud-run",
14195
+ accountKey: projectId || "validate",
14196
+ url: "https://logging.googleapis.com/v2/entries:list",
14197
+ token: String(credentials.accessToken ?? ""),
14198
+ init: {
14199
+ method: "POST",
14200
+ headers: { "Content-Type": "application/json" },
14201
+ body: JSON.stringify({ resourceNames: [`projects/${projectId}`], pageSize: 1 })
14202
+ },
14203
+ ...fetchImpl ? { fetchImpl } : {}
14204
+ });
14205
+ }
14206
+ },
14207
+ render: {
14208
+ provider: "render",
14209
+ primaryCredentialKey: "token",
14210
+ requiredCredentialFields: ["token"],
14211
+ requiredOptionFields: ["ownerId", "resourceId", "serviceName"],
14212
+ build(graph, options) {
14213
+ const config = options;
14214
+ return {
14215
+ connector: createRenderConnector(graph, config),
14216
+ resolveTarget: createRenderResolveTarget(config)
14217
+ };
14218
+ },
14219
+ // GET /v1/services?limit=1 — the cheapest read the Render API key
14220
+ // authenticates against (render.com/docs/api). Unlike Railway's GraphQL
14221
+ // gateway, Render is a plain REST API: a live key returns 2xx, a bad one a
14222
+ // 401/403, so authProbe's status-code check is a true verdict here. The
14223
+ // logs query itself also needs an ownerId + resource; `services` needs
14224
+ // neither and still fails 401 on a bad token, so it's the honest probe.
14225
+ validate({ credentials, options, fetchImpl }) {
14226
+ const cfg = options;
14227
+ const baseUrl = cfg.apiUrl ?? DEFAULT_RENDER_API_URL;
14228
+ return authProbe({
14229
+ provider: "render",
14230
+ accountKey: cfg.ownerId ?? "validate",
14231
+ url: `${baseUrl}/services?limit=1`,
14232
+ token: String(credentials.token ?? ""),
14233
+ ...fetchImpl ? { fetchImpl } : {}
14234
+ });
14235
+ }
13477
14236
  }
13478
14237
  };
13479
14238
  function vercelCredsFrom(credentials) {
@@ -13825,11 +14584,11 @@ function registerRoutes(scope, ctx) {
13825
14584
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
13826
14585
  const parsed = [];
13827
14586
  for (const c of candidates) {
13828
- const r = import_types59.DivergenceTypeSchema.safeParse(c);
14587
+ const r = import_types66.DivergenceTypeSchema.safeParse(c);
13829
14588
  if (!r.success) {
13830
14589
  return reply.code(400).send({
13831
14590
  error: `unknown divergence type "${c}"`,
13832
- allowed: import_types59.DivergenceTypeSchema.options
14591
+ allowed: import_types66.DivergenceTypeSchema.options
13833
14592
  });
13834
14593
  }
13835
14594
  parsed.push(r.data);
@@ -14138,7 +14897,7 @@ function registerRoutes(scope, ctx) {
14138
14897
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
14139
14898
  let violations = await log.readAll();
14140
14899
  if (req.query.severity) {
14141
- const sev = import_types59.PolicySeveritySchema.safeParse(req.query.severity);
14900
+ const sev = import_types66.PolicySeveritySchema.safeParse(req.query.severity);
14142
14901
  if (!sev.success) {
14143
14902
  return reply.code(400).send({
14144
14903
  error: "invalid severity",
@@ -14177,7 +14936,7 @@ function registerRoutes(scope, ctx) {
14177
14936
  scope.post("/policies/check", async (req, reply) => {
14178
14937
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
14179
14938
  if (!proj) return;
14180
- const parsed = import_types59.PoliciesCheckBodySchema.safeParse(req.body ?? {});
14939
+ const parsed = import_types66.PoliciesCheckBodySchema.safeParse(req.body ?? {});
14181
14940
  if (!parsed.success) {
14182
14941
  return reply.code(400).send({
14183
14942
  error: "invalid /policies/check body",
@@ -14526,7 +15285,7 @@ function unroutedErrorsPath(neatHome3) {
14526
15285
  }
14527
15286
 
14528
15287
  // src/daemon.ts
14529
- var import_types60 = require("@neat.is/types");
15288
+ var import_types67 = require("@neat.is/types");
14530
15289
  function daemonJsonPath(scanPath) {
14531
15290
  return import_node_path59.default.join(scanPath, "neat-out", "daemon.json");
14532
15291
  }
@@ -14651,7 +15410,7 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
14651
15410
  if (!serviceName) return true;
14652
15411
  if (serviceNameMatchesProject(serviceName, project)) return true;
14653
15412
  return graph.someNode(
14654
- (_id, attrs) => attrs.type === import_types60.NodeType.ServiceNode && attrs.name === serviceName
15413
+ (_id, attrs) => attrs.type === import_types67.NodeType.ServiceNode && attrs.name === serviceName
14655
15414
  );
14656
15415
  }
14657
15416
  async function bootstrapProject(entry, connectors = [], neatHome3) {