@neat.is/core 0.7.0 → 0.7.2

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.
@@ -1,8 +1,9 @@
1
1
  import {
2
+ columnsFromSqlStatement,
2
3
  mountBearerAuth,
3
4
  readAuthEnv,
4
5
  tableFromSqlStatement
5
- } from "./chunk-N5L3RBGP.js";
6
+ } from "./chunk-P2ZEKJ35.js";
6
7
 
7
8
  // src/graph.ts
8
9
  import GraphDefault from "graphology";
@@ -862,7 +863,7 @@ import {
862
863
  GraphEdgeSchema,
863
864
  GraphNodeSchema,
864
865
  NodeType as NodeType4,
865
- Provenance as Provenance3,
866
+ Provenance as Provenance4,
866
867
  confidenceForObservedSignal,
867
868
  databaseId,
868
869
  localDatabaseId,
@@ -1945,6 +1946,41 @@ async function addRoutes(graph, services) {
1945
1946
  return { nodesAdded, edgesAdded };
1946
1947
  }
1947
1948
 
1949
+ // src/columns.ts
1950
+ import { Provenance as Provenance3 } from "@neat.is/types";
1951
+ var OBSERVED_COLUMN_CONFIDENCE = 0.9;
1952
+ function normalizeProvenances(provenances) {
1953
+ return [...new Set(provenances)].sort();
1954
+ }
1955
+ function foldColumns(existing, names, provenance, confidence) {
1956
+ const out = (existing ?? []).map((c) => ({
1957
+ ...c,
1958
+ provenances: [...c.provenances]
1959
+ }));
1960
+ const byName = new Map(out.map((c) => [c.name, c]));
1961
+ for (const raw of names) {
1962
+ const name = raw.toLowerCase();
1963
+ const prior = byName.get(name);
1964
+ if (!prior) {
1965
+ const col = { name, provenances: [provenance], confidence };
1966
+ byName.set(name, col);
1967
+ out.push(col);
1968
+ continue;
1969
+ }
1970
+ if (!prior.provenances.includes(provenance)) {
1971
+ prior.provenances = normalizeProvenances([...prior.provenances, provenance]);
1972
+ }
1973
+ if (confidence > prior.confidence) prior.confidence = confidence;
1974
+ }
1975
+ return out;
1976
+ }
1977
+ function columnIsDeclared(col) {
1978
+ return col.provenances.includes(Provenance3.EXTRACTED);
1979
+ }
1980
+ function columnIsObserved(col) {
1981
+ return col.provenances.includes(Provenance3.OBSERVED);
1982
+ }
1983
+
1948
1984
  // src/ingest.ts
1949
1985
  var HOUR_MS = 60 * 60 * 1e3;
1950
1986
  var DAY_MS = 24 * HOUR_MS;
@@ -2283,7 +2319,7 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
2283
2319
  source: serviceNodeId,
2284
2320
  target: fileNodeId,
2285
2321
  type: EdgeType4.CONTAINS,
2286
- provenance: Provenance3.OBSERVED
2322
+ provenance: Provenance4.OBSERVED
2287
2323
  };
2288
2324
  graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
2289
2325
  }
@@ -2328,7 +2364,7 @@ function ensureObservedSymbolNode(graph, fileNodeId, service, relPath, fn, line)
2328
2364
  source: fileNodeId,
2329
2365
  target: sid,
2330
2366
  type: EdgeType4.CONTAINS,
2331
- provenance: Provenance3.OBSERVED
2367
+ provenance: Provenance4.OBSERVED
2332
2368
  };
2333
2369
  graph.addEdgeWithKey(containsId, fileNodeId, sid, edge);
2334
2370
  }
@@ -2543,6 +2579,21 @@ function ensureInfraNode(graph, kind, name, provider) {
2543
2579
  graph.addNode(id, node);
2544
2580
  return id;
2545
2581
  }
2582
+ var COLUMN_BEARING_INFRA_KINDS = /* @__PURE__ */ new Set(["sql-table", "supabase-table"]);
2583
+ function mergeColumnsAt(graph, tableNodeId, columns, provenance, confidence) {
2584
+ if (!columns || columns.length === 0 || !graph.hasNode(tableNodeId)) return;
2585
+ const node = graph.getNodeAttributes(tableNodeId);
2586
+ if (node.type !== NodeType4.InfraNode || !node.kind || !COLUMN_BEARING_INFRA_KINDS.has(node.kind)) {
2587
+ return;
2588
+ }
2589
+ graph.replaceNodeAttributes(tableNodeId, {
2590
+ ...node,
2591
+ columns: foldColumns(node.columns, columns, provenance, confidence)
2592
+ });
2593
+ }
2594
+ function mergeObservedColumns(graph, tableNodeId, columns) {
2595
+ mergeColumnsAt(graph, tableNodeId, columns, Provenance4.OBSERVED, OBSERVED_COLUMN_CONFIDENCE);
2596
+ }
2546
2597
  function ensureDatabaseNode(graph, host, engine) {
2547
2598
  const id = databaseId(host);
2548
2599
  if (graph.hasNode(id)) return id;
@@ -2586,7 +2637,7 @@ function findDeclaredDatabaseForService(graph, serviceNodeId, engine) {
2586
2637
  if (!graph.hasNode(src)) continue;
2587
2638
  for (const edgeId of graph.outboundEdges(src)) {
2588
2639
  const edge = graph.getEdgeAttributes(edgeId);
2589
- if (edge.type !== EdgeType4.CONNECTS_TO || edge.provenance !== Provenance3.EXTRACTED) continue;
2640
+ if (edge.type !== EdgeType4.CONNECTS_TO || edge.provenance !== Provenance4.EXTRACTED) continue;
2590
2641
  if (!graph.hasNode(edge.target)) continue;
2591
2642
  const target = graph.getNodeAttributes(edge.target);
2592
2643
  if (target.type !== NodeType4.DatabaseNode || target.engine !== engine) continue;
@@ -2628,7 +2679,7 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
2628
2679
  };
2629
2680
  const updated = {
2630
2681
  ...existing,
2631
- provenance: Provenance3.OBSERVED,
2682
+ provenance: Provenance4.OBSERVED,
2632
2683
  lastObserved: ts,
2633
2684
  callCount: newSpanCount,
2634
2685
  signal: newSignal,
@@ -2649,7 +2700,7 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
2649
2700
  source,
2650
2701
  target,
2651
2702
  type,
2652
- provenance: Provenance3.OBSERVED,
2703
+ provenance: Provenance4.OBSERVED,
2653
2704
  confidence: confidenceForObservedSignal(signal),
2654
2705
  lastObserved: ts,
2655
2706
  callCount: 1,
@@ -2672,7 +2723,7 @@ function stitchTrace(graph, sourceServiceId, ts) {
2672
2723
  const outbound = graph.outboundEdges(nodeId);
2673
2724
  for (const edgeId of outbound) {
2674
2725
  const edge = graph.getEdgeAttributes(edgeId);
2675
- if (edge.provenance !== Provenance3.EXTRACTED) continue;
2726
+ if (edge.provenance !== Provenance4.EXTRACTED) continue;
2676
2727
  if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
2677
2728
  if (graph.hasEdge(observedEdgeId(edge.source, edge.target, edge.type))) continue;
2678
2729
  upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
@@ -2696,7 +2747,7 @@ function upsertInferredEdge(graph, type, source, target, ts) {
2696
2747
  source,
2697
2748
  target,
2698
2749
  type,
2699
- provenance: Provenance3.INFERRED,
2750
+ provenance: Provenance4.INFERRED,
2700
2751
  confidence: INFERRED_CONFIDENCE,
2701
2752
  lastObserved: ts
2702
2753
  };
@@ -2916,6 +2967,7 @@ async function handleSpan(ctx, span) {
2916
2967
  isError,
2917
2968
  callSiteEvidence
2918
2969
  );
2970
+ mergeObservedColumns(ctx.graph, tableId, span.dbColumns);
2919
2971
  }
2920
2972
  }
2921
2973
  } else if (span.messagingSystem && span.messagingDestination && spanMintsMessagingEdge(span.kind)) {
@@ -3037,7 +3089,7 @@ async function handleSpan(ctx, span) {
3037
3089
  }
3038
3090
  }
3039
3091
  }
3040
- if (span.httpRoute && (span.kind === 2 || span.kind === 0 || span.kind === void 0)) {
3092
+ if (span.httpRoute && (span.kind === 2 || span.kind === 1 || span.kind === 0 || span.kind === void 0)) {
3041
3093
  const routeNodeId = findRouteNodeByHttpRoute(
3042
3094
  ctx.graph,
3043
3095
  span.service,
@@ -3128,7 +3180,7 @@ function rewireFrontierEdges(graph, frontierId2, serviceId7) {
3128
3180
  }
3129
3181
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
3130
3182
  graph.dropEdge(oldEdgeId);
3131
- const newId = edge.provenance === Provenance3.OBSERVED ? observedEdgeId(newSource, newTarget, edge.type) : edge.provenance === Provenance3.INFERRED ? inferredEdgeId(newSource, newTarget, edge.type) : extractedEdgeId4(newSource, newTarget, edge.type);
3183
+ const newId = edge.provenance === Provenance4.OBSERVED ? observedEdgeId(newSource, newTarget, edge.type) : edge.provenance === Provenance4.INFERRED ? inferredEdgeId(newSource, newTarget, edge.type) : extractedEdgeId4(newSource, newTarget, edge.type);
3132
3184
  if (graph.hasEdge(newId)) {
3133
3185
  const existing = graph.getEdgeAttributes(newId);
3134
3186
  const merged = {
@@ -3162,12 +3214,12 @@ async function markStaleEdges(graph, options = {}) {
3162
3214
  const project = options.project ?? DEFAULT_PROJECT;
3163
3215
  graph.forEachEdge((id, attrs) => {
3164
3216
  const e = attrs;
3165
- if (e.provenance !== Provenance3.OBSERVED) return;
3217
+ if (e.provenance !== Provenance4.OBSERVED) return;
3166
3218
  if (!e.lastObserved) return;
3167
3219
  const threshold = thresholdForEdgeType(e.type, thresholds);
3168
3220
  const age = now - new Date(e.lastObserved).getTime();
3169
3221
  if (age > threshold) {
3170
- const updated = { ...e, provenance: Provenance3.STALE, confidence: 0.3 };
3222
+ const updated = { ...e, provenance: Provenance4.STALE, confidence: 0.3 };
3171
3223
  graph.replaceEdgeAttributes(id, updated);
3172
3224
  events.push({
3173
3225
  edgeId: id,
@@ -3184,8 +3236,8 @@ async function markStaleEdges(graph, options = {}) {
3184
3236
  project,
3185
3237
  payload: {
3186
3238
  edgeId: id,
3187
- from: Provenance3.OBSERVED,
3188
- to: Provenance3.STALE
3239
+ from: Provenance4.OBSERVED,
3240
+ to: Provenance4.STALE
3189
3241
  }
3190
3242
  });
3191
3243
  }
@@ -3338,7 +3390,7 @@ import {
3338
3390
  NodeType as NodeType5,
3339
3391
  ObservedDependenciesResultSchema,
3340
3392
  PROV_RANK,
3341
- Provenance as Provenance4,
3393
+ Provenance as Provenance5,
3342
3394
  RootCauseResultSchema,
3343
3395
  TransitiveDependenciesResultSchema
3344
3396
  } from "@neat.is/types";
@@ -3451,19 +3503,19 @@ function confidenceFromMix(edges, now = Date.now()) {
3451
3503
  function longestIncomingWalk(graph, start, maxDepth) {
3452
3504
  let best = { path: [start], edges: [] };
3453
3505
  const visited = /* @__PURE__ */ new Set([start]);
3454
- function step(node, path54, edges) {
3455
- if (path54.length > best.path.length) {
3456
- best = { path: [...path54], edges: [...edges] };
3506
+ function step(node, path55, edges) {
3507
+ if (path55.length > best.path.length) {
3508
+ best = { path: [...path55], edges: [...edges] };
3457
3509
  }
3458
- if (path54.length - 1 >= maxDepth) return;
3510
+ if (path55.length - 1 >= maxDepth) return;
3459
3511
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
3460
3512
  for (const [srcId, edge] of incoming) {
3461
3513
  if (visited.has(srcId)) continue;
3462
3514
  visited.add(srcId);
3463
- path54.push(srcId);
3515
+ path55.push(srcId);
3464
3516
  edges.push(edge);
3465
- step(srcId, path54, edges);
3466
- path54.pop();
3517
+ step(srcId, path55, edges);
3518
+ path55.pop();
3467
3519
  edges.pop();
3468
3520
  visited.delete(srcId);
3469
3521
  }
@@ -3614,7 +3666,7 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
3614
3666
  const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
3615
3667
  if (!loc) return null;
3616
3668
  const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
3617
- const edgeProvenances = loc.fileNode ? [Provenance4.OBSERVED] : [];
3669
+ const edgeProvenances = loc.fileNode ? [Provenance5.OBSERVED] : [];
3618
3670
  return RootCauseResultSchema.parse({
3619
3671
  rootCauseNode: loc.rootCauseNode,
3620
3672
  rootCauseReason: loc.rootCauseReason,
@@ -3670,26 +3722,26 @@ function dominantFailingCall(graph, serviceId7, visited) {
3670
3722
  return best;
3671
3723
  }
3672
3724
  function followFailingCallChain(graph, originServiceId, maxDepth) {
3673
- const path54 = [originServiceId];
3725
+ const path55 = [originServiceId];
3674
3726
  const edges = [];
3675
3727
  const visited = /* @__PURE__ */ new Set([originServiceId]);
3676
3728
  let current = originServiceId;
3677
3729
  for (let depth = 0; depth < maxDepth; depth++) {
3678
3730
  const hop = dominantFailingCall(graph, current, visited);
3679
3731
  if (!hop) break;
3680
- path54.push(hop.nextService);
3732
+ path55.push(hop.nextService);
3681
3733
  edges.push(hop.edge);
3682
3734
  visited.add(hop.nextService);
3683
3735
  current = hop.nextService;
3684
3736
  }
3685
3737
  if (edges.length === 0) return null;
3686
- return { path: path54, edges, culprit: current };
3738
+ return { path: path55, edges, culprit: current };
3687
3739
  }
3688
3740
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
3689
3741
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
3690
3742
  if (!chain) return null;
3691
3743
  const culprit = chain.culprit;
3692
- const path54 = [...chain.path];
3744
+ const path55 = [...chain.path];
3693
3745
  const edgeProvenances = chain.edges.map((e) => e.provenance);
3694
3746
  const baseConfidence = confidenceFromMix(chain.edges);
3695
3747
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -3697,14 +3749,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
3697
3749
  if (loc) {
3698
3750
  let rootCauseNode = culprit;
3699
3751
  if (loc.fileNode) {
3700
- path54.push(loc.fileNode);
3701
- edgeProvenances.push(Provenance4.OBSERVED);
3752
+ path55.push(loc.fileNode);
3753
+ edgeProvenances.push(Provenance5.OBSERVED);
3702
3754
  rootCauseNode = loc.fileNode;
3703
3755
  }
3704
3756
  return RootCauseResultSchema.parse({
3705
3757
  rootCauseNode,
3706
3758
  rootCauseReason: loc.rootCauseReason,
3707
- traversalPath: path54,
3759
+ traversalPath: path55,
3708
3760
  edgeProvenances,
3709
3761
  confidence,
3710
3762
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -3716,7 +3768,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
3716
3768
  return RootCauseResultSchema.parse({
3717
3769
  rootCauseNode: culprit,
3718
3770
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
3719
- traversalPath: path54,
3771
+ traversalPath: path55,
3720
3772
  edgeProvenances,
3721
3773
  confidence,
3722
3774
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -3832,12 +3884,12 @@ function getObservedDependencies(graph, nodeId) {
3832
3884
  for (const edgeId of graph.outboundEdges(src)) {
3833
3885
  const e = graph.getEdgeAttributes(edgeId);
3834
3886
  if (e.type === EdgeType5.CONTAINS) continue;
3835
- if (e.provenance === Provenance4.OBSERVED) {
3887
+ if (e.provenance === Provenance5.OBSERVED) {
3836
3888
  if (!seenEdge.has(e.id)) {
3837
3889
  seenEdge.add(e.id);
3838
3890
  dependencies.push(e);
3839
3891
  }
3840
- } else if (e.provenance === Provenance4.EXTRACTED) {
3892
+ } else if (e.provenance === Provenance5.EXTRACTED) {
3841
3893
  hasExtractedOutbound = true;
3842
3894
  }
3843
3895
  }
@@ -3847,7 +3899,7 @@ function getObservedDependencies(graph, nodeId) {
3847
3899
  for (const edgeId of graph.inboundEdges(tgt)) {
3848
3900
  const e = graph.getEdgeAttributes(edgeId);
3849
3901
  if (e.type === EdgeType5.CONTAINS) continue;
3850
- if (e.provenance === Provenance4.OBSERVED) inboundObservedCount += 1;
3902
+ if (e.provenance === Provenance5.OBSERVED) inboundObservedCount += 1;
3851
3903
  }
3852
3904
  }
3853
3905
  dependencies.sort(
@@ -4457,7 +4509,7 @@ import TypeScript from "tree-sitter-typescript";
4457
4509
  import {
4458
4510
  EdgeType as EdgeType6,
4459
4511
  NodeType as NodeType9,
4460
- Provenance as Provenance5,
4512
+ Provenance as Provenance6,
4461
4513
  confidenceForExtracted as confidenceForExtracted3,
4462
4514
  extractedEdgeId as extractedEdgeId5,
4463
4515
  symbolId as symbolId2
@@ -4552,7 +4604,7 @@ function disambiguate(defs) {
4552
4604
  }
4553
4605
  async function addSymbols(graph, services) {
4554
4606
  const parsers = /* @__PURE__ */ new Map();
4555
- const parserForExt = (ext) => {
4607
+ const parserForExt2 = (ext) => {
4556
4608
  const grammar = GRAMMAR_BY_EXT[ext];
4557
4609
  if (!grammar) return null;
4558
4610
  let parser = parsers.get(ext);
@@ -4568,7 +4620,7 @@ async function addSymbols(graph, services) {
4568
4620
  for (const service of services) {
4569
4621
  const files = await loadSourceFiles(service.dir);
4570
4622
  for (const file of files) {
4571
- const parser = parserForExt(path14.extname(file.path));
4623
+ const parser = parserForExt2(path14.extname(file.path));
4572
4624
  if (!parser) continue;
4573
4625
  const relPath = toPosix(path14.relative(service.dir, file.path));
4574
4626
  let defs;
@@ -4611,7 +4663,7 @@ async function addSymbols(graph, services) {
4611
4663
  source: fileNodeId,
4612
4664
  target: sid,
4613
4665
  type: EdgeType6.CONTAINS,
4614
- provenance: Provenance5.EXTRACTED,
4666
+ provenance: Provenance6.EXTRACTED,
4615
4667
  confidence: confidenceForExtracted3("structural"),
4616
4668
  evidence: {
4617
4669
  file: relPath,
@@ -4634,7 +4686,7 @@ import Parser4 from "tree-sitter";
4634
4686
  import {
4635
4687
  EdgeType as EdgeType8,
4636
4688
  NodeType as NodeType10,
4637
- Provenance as Provenance7,
4689
+ Provenance as Provenance8,
4638
4690
  confidenceForExtracted as confidenceForExtracted5,
4639
4691
  extractedEdgeId as extractedEdgeId7,
4640
4692
  symbolId as symbolId3
@@ -4649,7 +4701,7 @@ import Python2 from "tree-sitter-python";
4649
4701
  import Go2 from "tree-sitter-go";
4650
4702
  import {
4651
4703
  EdgeType as EdgeType7,
4652
- Provenance as Provenance6,
4704
+ Provenance as Provenance7,
4653
4705
  confidenceForExtracted as confidenceForExtracted4,
4654
4706
  extractedEdgeId as extractedEdgeId6,
4655
4707
  fileId as fileId3
@@ -4936,7 +4988,7 @@ function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, imp
4936
4988
  source: importerFileId,
4937
4989
  target: importeeFileId,
4938
4990
  type: EdgeType7.IMPORTS,
4939
- provenance: Provenance6.EXTRACTED,
4991
+ provenance: Provenance7.EXTRACTED,
4940
4992
  confidence: confidenceForExtracted4("structural"),
4941
4993
  evidence: { file: importerRelPath, line, snippet: snippet2 }
4942
4994
  };
@@ -5115,7 +5167,7 @@ function stringInner(node) {
5115
5167
  }
5116
5168
  async function addSymbolEdges(graph, services) {
5117
5169
  const parsers = /* @__PURE__ */ new Map();
5118
- const parserForExt = (ext) => {
5170
+ const parserForExt2 = (ext) => {
5119
5171
  const grammar = GRAMMAR_BY_EXT[ext];
5120
5172
  if (!grammar) return null;
5121
5173
  let parser = parsers.get(ext);
@@ -5132,7 +5184,7 @@ async function addSymbolEdges(graph, services) {
5132
5184
  const tsPaths = await loadTsPathConfig(service.dir);
5133
5185
  const files = await loadSourceFiles(service.dir);
5134
5186
  for (const file of files) {
5135
- const parser = parserForExt(path16.extname(file.path));
5187
+ const parser = parserForExt2(path16.extname(file.path));
5136
5188
  if (!parser) continue;
5137
5189
  const relPath = toPosix(path16.relative(service.dir, file.path));
5138
5190
  const fileDir = path16.dirname(file.path);
@@ -5252,7 +5304,7 @@ async function addSymbolEdges(graph, services) {
5252
5304
  source: req.sourceSid,
5253
5305
  target: targetSid,
5254
5306
  type: req.edgeType,
5255
- provenance: Provenance7.EXTRACTED,
5307
+ provenance: Provenance8.EXTRACTED,
5256
5308
  confidence: confidenceForExtracted5("structural"),
5257
5309
  evidence: {
5258
5310
  file: relPath,
@@ -5273,7 +5325,7 @@ import path24 from "path";
5273
5325
  import {
5274
5326
  EdgeType as EdgeType9,
5275
5327
  NodeType as NodeType11,
5276
- Provenance as Provenance8,
5328
+ Provenance as Provenance9,
5277
5329
  configId,
5278
5330
  databaseId as databaseId2,
5279
5331
  confidenceForExtracted as confidenceForExtracted6
@@ -5917,7 +5969,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
5917
5969
  source: fileNodeId,
5918
5970
  target: dbNode.id,
5919
5971
  type: EdgeType9.CONNECTS_TO,
5920
- provenance: Provenance8.EXTRACTED,
5972
+ provenance: Provenance9.EXTRACTED,
5921
5973
  confidence: confidenceForExtracted6("structural"),
5922
5974
  evidence: { file: evidenceFile }
5923
5975
  };
@@ -5947,7 +5999,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
5947
5999
  source: service.node.id,
5948
6000
  target: cfgId,
5949
6001
  type: EdgeType9.CONFIGURED_BY,
5950
- provenance: Provenance8.EXTRACTED,
6002
+ provenance: Provenance9.EXTRACTED,
5951
6003
  confidence: confidenceForExtracted6("structural"),
5952
6004
  evidence: { file: toPosix(relPath) }
5953
6005
  };
@@ -5982,7 +6034,7 @@ import path25 from "path";
5982
6034
  import {
5983
6035
  EdgeType as EdgeType10,
5984
6036
  NodeType as NodeType12,
5985
- Provenance as Provenance9,
6037
+ Provenance as Provenance10,
5986
6038
  configId as configId2,
5987
6039
  confidenceForExtracted as confidenceForExtracted7
5988
6040
  } from "@neat.is/types";
@@ -6036,7 +6088,7 @@ async function addConfigNodes(graph, services, scanPath) {
6036
6088
  source: fileNodeId,
6037
6089
  target: node.id,
6038
6090
  type: EdgeType10.CONFIGURED_BY,
6039
- provenance: Provenance9.EXTRACTED,
6091
+ provenance: Provenance10.EXTRACTED,
6040
6092
  confidence: confidenceForExtracted7("structural"),
6041
6093
  evidence: { file: relPath.split(path25.sep).join("/") }
6042
6094
  };
@@ -6055,7 +6107,7 @@ import path26 from "path";
6055
6107
  import {
6056
6108
  EdgeType as EdgeType11,
6057
6109
  NodeType as NodeType13,
6058
- Provenance as Provenance10,
6110
+ Provenance as Provenance11,
6059
6111
  confidenceForExtracted as confidenceForExtracted8,
6060
6112
  extractedEdgeId as extractedEdgeId8,
6061
6113
  grpcMethodId as grpcMethodId2
@@ -6159,7 +6211,7 @@ async function addGrpcMethods(graph, services) {
6159
6211
  source: service.node.id,
6160
6212
  target: mid,
6161
6213
  type: EdgeType11.CONTAINS,
6162
- provenance: Provenance10.EXTRACTED,
6214
+ provenance: Provenance11.EXTRACTED,
6163
6215
  confidence: confidenceForExtracted8("structural"),
6164
6216
  evidence: {
6165
6217
  file: relFile,
@@ -6180,7 +6232,7 @@ async function addGrpcMethods(graph, services) {
6180
6232
  import {
6181
6233
  EdgeType as EdgeType14,
6182
6234
  NodeType as NodeType15,
6183
- Provenance as Provenance13,
6235
+ Provenance as Provenance14,
6184
6236
  confidenceForExtracted as confidenceForExtracted11,
6185
6237
  passesExtractedFloor as passesExtractedFloor3
6186
6238
  } from "@neat.is/types";
@@ -6192,7 +6244,7 @@ import JavaScript4 from "tree-sitter-javascript";
6192
6244
  import Python3 from "tree-sitter-python";
6193
6245
  import {
6194
6246
  EdgeType as EdgeType12,
6195
- Provenance as Provenance11,
6247
+ Provenance as Provenance12,
6196
6248
  confidenceForExtracted as confidenceForExtracted9,
6197
6249
  passesExtractedFloor
6198
6250
  } from "@neat.is/types";
@@ -6313,7 +6365,7 @@ async function addHttpCallEdges(graph, services) {
6313
6365
  source: fileNodeId,
6314
6366
  target: targetId,
6315
6367
  type: EdgeType12.CALLS,
6316
- provenance: Provenance11.EXTRACTED,
6368
+ provenance: Provenance12.EXTRACTED,
6317
6369
  confidence,
6318
6370
  evidence: ev
6319
6371
  };
@@ -6333,7 +6385,7 @@ import JavaScript5 from "tree-sitter-javascript";
6333
6385
  import {
6334
6386
  EdgeType as EdgeType13,
6335
6387
  NodeType as NodeType14,
6336
- Provenance as Provenance12,
6388
+ Provenance as Provenance13,
6337
6389
  confidenceForExtracted as confidenceForExtracted10,
6338
6390
  passesExtractedFloor as passesExtractedFloor2,
6339
6391
  serviceId as serviceId4
@@ -6588,7 +6640,7 @@ async function addRouteCallEdges(graph, services) {
6588
6640
  source: fileNodeId,
6589
6641
  target: match.routeNodeId,
6590
6642
  type: EdgeType13.CALLS,
6591
- provenance: Provenance12.EXTRACTED,
6643
+ provenance: Provenance13.EXTRACTED,
6592
6644
  confidence,
6593
6645
  evidence: ev
6594
6646
  };
@@ -7260,12 +7312,48 @@ function walk3(node, visit) {
7260
7312
  visit(node);
7261
7313
  for (const c of namedChildren(node)) walk3(c, visit);
7262
7314
  }
7315
+ var COLUMN_BUILDERS = /* @__PURE__ */ new Set(["Column", "mapped_column"]);
7316
+ function isColumnBuilder(call) {
7317
+ const fn = call.childForFieldName("function");
7318
+ const t = fn?.text;
7319
+ if (!t) return false;
7320
+ const base = t.includes(".") ? t.slice(t.lastIndexOf(".") + 1) : t;
7321
+ return COLUMN_BUILDERS.has(base);
7322
+ }
7323
+ function firstPositionalString(call) {
7324
+ const args = call.childForFieldName("arguments");
7325
+ if (!args) return null;
7326
+ for (const arg of namedChildren(args)) {
7327
+ if (arg.type === "keyword_argument") continue;
7328
+ return arg.type === "string" ? pyStaticStringText2(arg) : null;
7329
+ }
7330
+ return null;
7331
+ }
7332
+ function columnsFromClassBody(body) {
7333
+ const out = [];
7334
+ const seen = /* @__PURE__ */ new Set();
7335
+ for (const stmt of namedChildren(body)) {
7336
+ if (stmt.type !== "expression_statement") continue;
7337
+ const assign = stmt.namedChild(0);
7338
+ if (assign?.type !== "assignment") continue;
7339
+ const left = assign.childForFieldName("left");
7340
+ if (left?.type !== "identifier") continue;
7341
+ const right = assign.childForFieldName("right");
7342
+ if (right?.type !== "call" || !isColumnBuilder(right)) continue;
7343
+ const name = firstPositionalString(right) ?? left.text;
7344
+ if (name && !seen.has(name)) {
7345
+ seen.add(name);
7346
+ out.push(name);
7347
+ }
7348
+ }
7349
+ return out;
7350
+ }
7263
7351
  function sqlalchemyEndpointsFromFile(file, serviceDir) {
7264
7352
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
7265
7353
  const tree = parseSource6(makePyParser4(), file.content);
7266
7354
  const out = [];
7267
7355
  const seen = /* @__PURE__ */ new Set();
7268
- const push = (name, line) => {
7356
+ const push = (name, line, columns) => {
7269
7357
  if (seen.has(name)) return;
7270
7358
  seen.add(name);
7271
7359
  out.push({
@@ -7274,6 +7362,7 @@ function sqlalchemyEndpointsFromFile(file, serviceDir) {
7274
7362
  kind: "sql-table",
7275
7363
  edgeType: "CALLS",
7276
7364
  confidenceKind: "verified-call-site",
7365
+ ...columns && columns.length > 0 ? { columns } : {},
7277
7366
  evidence: {
7278
7367
  file: path35.relative(serviceDir, file.path),
7279
7368
  line,
@@ -7289,11 +7378,12 @@ function sqlalchemyEndpointsFromFile(file, serviceDir) {
7289
7378
  const line = node.startPosition.row + 1;
7290
7379
  const explicit = explicitTablename(body);
7291
7380
  if (explicit === "computed") return;
7381
+ const columns = columnsFromClassBody(body);
7292
7382
  if (explicit) {
7293
- push(explicit.name, line);
7383
+ push(explicit.name, line, columns);
7294
7384
  return;
7295
7385
  }
7296
- if (extendsFlaskModel(node)) push(flaskSqlalchemyTableName(nameNode.text), line);
7386
+ if (extendsFlaskModel(node)) push(flaskSqlalchemyTableName(nameNode.text), line, columns);
7297
7387
  return;
7298
7388
  }
7299
7389
  if (node.type === "call") {
@@ -7486,11 +7576,127 @@ function djangoOrmEndpointsFromFile(file, serviceDir) {
7486
7576
  return out;
7487
7577
  }
7488
7578
 
7489
- // src/extract/calls/go.ts
7579
+ // src/extract/calls/drizzle.ts
7490
7580
  import path37 from "path";
7491
7581
  import Parser9 from "tree-sitter";
7492
- import Go3 from "tree-sitter-go";
7582
+ import JavaScript6 from "tree-sitter-javascript";
7493
7583
  import { infraId as infraId10 } from "@neat.is/types";
7584
+ var DRIZZLE_IMPORT_RE = /drizzle-orm/;
7585
+ var TABLE_BUILDERS = /* @__PURE__ */ new Set(["pgTable", "mysqlTable", "sqliteTable"]);
7586
+ function parserForExt(ext) {
7587
+ const p = new Parser9();
7588
+ p.setLanguage(GRAMMAR_BY_EXT[ext] ?? JavaScript6);
7589
+ return p;
7590
+ }
7591
+ function namedChildren3(node) {
7592
+ const out = [];
7593
+ for (let i = 0; i < node.namedChildCount; i++) {
7594
+ const c = node.namedChild(i);
7595
+ if (c) out.push(c);
7596
+ }
7597
+ return out;
7598
+ }
7599
+ function stringLiteralText2(node) {
7600
+ if (!node || node.type !== "string") return null;
7601
+ for (const child of namedChildren3(node)) {
7602
+ if (child.type === "string_fragment") return child.text;
7603
+ }
7604
+ return "";
7605
+ }
7606
+ function firstStringArg(call) {
7607
+ const args = call.childForFieldName("arguments");
7608
+ if (!args) return null;
7609
+ for (const arg of namedChildren3(args)) {
7610
+ if (arg.type === "string") return stringLiteralText2(arg);
7611
+ return null;
7612
+ }
7613
+ return null;
7614
+ }
7615
+ function builderColumnName(value) {
7616
+ let node = value;
7617
+ while (node) {
7618
+ if (node.type === "call_expression") {
7619
+ const fn = node.childForFieldName("function");
7620
+ if (fn?.type === "identifier") return firstStringArg(node);
7621
+ if (fn?.type === "member_expression") {
7622
+ node = fn.childForFieldName("object");
7623
+ continue;
7624
+ }
7625
+ return null;
7626
+ }
7627
+ if (node.type === "member_expression") {
7628
+ node = node.childForFieldName("object");
7629
+ continue;
7630
+ }
7631
+ return null;
7632
+ }
7633
+ return null;
7634
+ }
7635
+ function keyName(key) {
7636
+ if (!key) return null;
7637
+ if (key.type === "property_identifier") return key.text;
7638
+ if (key.type === "string") return stringLiteralText2(key);
7639
+ return null;
7640
+ }
7641
+ function columnsFromObject(obj) {
7642
+ const out = [];
7643
+ const seen = /* @__PURE__ */ new Set();
7644
+ for (const child of namedChildren3(obj)) {
7645
+ if (child.type !== "pair") continue;
7646
+ const value = child.childForFieldName("value");
7647
+ const key = keyName(child.childForFieldName("key"));
7648
+ const name = (value ? builderColumnName(value) : null) ?? key;
7649
+ if (name && !seen.has(name)) {
7650
+ seen.add(name);
7651
+ out.push(name);
7652
+ }
7653
+ }
7654
+ return out;
7655
+ }
7656
+ function drizzleEndpointsFromFile(file, serviceDir) {
7657
+ if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
7658
+ const tree = parseSource2(parserForExt(path37.extname(file.path)), file.content);
7659
+ const out = [];
7660
+ const seen = /* @__PURE__ */ new Set();
7661
+ const walk6 = (node) => {
7662
+ if (node.type === "call_expression") {
7663
+ const fn = node.childForFieldName("function");
7664
+ if (fn?.type === "identifier" && TABLE_BUILDERS.has(fn.text)) {
7665
+ const args = node.childForFieldName("arguments");
7666
+ const argNodes = args ? namedChildren3(args) : [];
7667
+ const tableName = stringLiteralText2(argNodes[0] ?? null);
7668
+ const obj = argNodes[1];
7669
+ if (tableName && obj?.type === "object" && !seen.has(tableName)) {
7670
+ seen.add(tableName);
7671
+ const columns = columnsFromObject(obj);
7672
+ const line = node.startPosition.row + 1;
7673
+ out.push({
7674
+ infraId: infraId10("sql-table", tableName),
7675
+ name: tableName,
7676
+ kind: "sql-table",
7677
+ edgeType: "CALLS",
7678
+ confidenceKind: "structural",
7679
+ columns,
7680
+ evidence: {
7681
+ file: path37.relative(serviceDir, file.path),
7682
+ line,
7683
+ snippet: snippet(file.content, line)
7684
+ }
7685
+ });
7686
+ }
7687
+ }
7688
+ }
7689
+ for (const c of namedChildren3(node)) walk6(c);
7690
+ };
7691
+ walk6(tree.rootNode);
7692
+ return out;
7693
+ }
7694
+
7695
+ // src/extract/calls/go.ts
7696
+ import path38 from "path";
7697
+ import Parser10 from "tree-sitter";
7698
+ import Go3 from "tree-sitter-go";
7699
+ import { infraId as infraId11 } from "@neat.is/types";
7494
7700
  var SQL_METHODS = /* @__PURE__ */ new Set(["Exec", "ExecContext", "Query", "QueryContext", "QueryRow", "QueryRowContext"]);
7495
7701
  var PARSE_CHUNK8 = 16384;
7496
7702
  function walk5(node, visit) {
@@ -7501,8 +7707,8 @@ function walk5(node, visit) {
7501
7707
  }
7502
7708
  }
7503
7709
  function goSqlEndpointsFromFile(file, serviceDir) {
7504
- if (path37.extname(file.path) !== ".go") return [];
7505
- const parser = new Parser9();
7710
+ if (path38.extname(file.path) !== ".go") return [];
7711
+ const parser = new Parser10();
7506
7712
  parser.setLanguage(Go3);
7507
7713
  const tree = parser.parse(
7508
7714
  (index) => index >= file.content.length ? "" : file.content.slice(index, index + PARSE_CHUNK8)
@@ -7521,12 +7727,12 @@ function goSqlEndpointsFromFile(file, serviceDir) {
7521
7727
  if (!table) return;
7522
7728
  const line = node.startPosition.row + 1;
7523
7729
  out.push({
7524
- infraId: infraId10("sql-table", table),
7730
+ infraId: infraId11("sql-table", table),
7525
7731
  name: table,
7526
7732
  kind: "sql-table",
7527
7733
  edgeType: "CALLS",
7528
7734
  confidenceKind: "verified-call-site",
7529
- evidence: { file: toPosix(path37.relative(serviceDir, file.path)), line, snippet: snippet(file.content, line) }
7735
+ evidence: { file: toPosix(path38.relative(serviceDir, file.path)), line, snippet: snippet(file.content, line) }
7530
7736
  });
7531
7737
  });
7532
7738
  return out;
@@ -7566,6 +7772,7 @@ async function addExternalEndpointEdges(graph, services) {
7566
7772
  endpoints.push(...mongooseEndpointsFromFile(maskedFile, service.dir));
7567
7773
  endpoints.push(...sqlalchemyEndpointsFromFile(maskedFile, service.dir));
7568
7774
  endpoints.push(...djangoOrmEndpointsFromFile(maskedFile, service.dir));
7775
+ endpoints.push(...drizzleEndpointsFromFile(maskedFile, service.dir));
7569
7776
  try {
7570
7777
  endpoints.push(...goSqlEndpointsFromFile(maskedFile, service.dir));
7571
7778
  } catch (err) {
@@ -7591,6 +7798,20 @@ async function addExternalEndpointEdges(graph, services) {
7591
7798
  graph.addNode(node.id, node);
7592
7799
  nodesAdded++;
7593
7800
  }
7801
+ if (ep.columns && ep.columns.length > 0) {
7802
+ const node = graph.getNodeAttributes(ep.infraId);
7803
+ if (node.type === NodeType15.InfraNode) {
7804
+ graph.replaceNodeAttributes(ep.infraId, {
7805
+ ...node,
7806
+ columns: foldColumns(
7807
+ node.columns,
7808
+ ep.columns,
7809
+ Provenance14.EXTRACTED,
7810
+ confidenceForExtracted11(ep.confidenceKind)
7811
+ )
7812
+ });
7813
+ }
7814
+ }
7594
7815
  const edgeType = edgeTypeFromEndpoint(ep);
7595
7816
  const confidence = confidenceForExtracted11(ep.confidenceKind);
7596
7817
  const relFile = toPosix(ep.evidence.file);
@@ -7622,7 +7843,7 @@ async function addExternalEndpointEdges(graph, services) {
7622
7843
  source: fileNodeId,
7623
7844
  target: ep.infraId,
7624
7845
  type: edgeType,
7625
- provenance: Provenance13.EXTRACTED,
7846
+ provenance: Provenance14.EXTRACTED,
7626
7847
  confidence,
7627
7848
  evidence: ep.evidence
7628
7849
  };
@@ -7644,14 +7865,14 @@ async function addCallEdges(graph, services) {
7644
7865
  }
7645
7866
 
7646
7867
  // src/extract/infra/docker-compose.ts
7647
- import path38 from "path";
7648
- import { EdgeType as EdgeType15, Provenance as Provenance15, confidenceForExtracted as confidenceForExtracted13 } from "@neat.is/types";
7868
+ import path39 from "path";
7869
+ import { EdgeType as EdgeType15, Provenance as Provenance16, confidenceForExtracted as confidenceForExtracted13 } from "@neat.is/types";
7649
7870
 
7650
7871
  // src/extract/infra/shared.ts
7651
- import { NodeType as NodeType16, Provenance as Provenance14, confidenceForExtracted as confidenceForExtracted12, infraId as infraId11 } from "@neat.is/types";
7872
+ import { NodeType as NodeType16, Provenance as Provenance15, confidenceForExtracted as confidenceForExtracted12, infraId as infraId12 } from "@neat.is/types";
7652
7873
  function makeInfraNode(kind, name, provider = "self", extras) {
7653
7874
  return {
7654
- id: infraId11(kind, name),
7875
+ id: infraId12(kind, name),
7655
7876
  type: NodeType16.InfraNode,
7656
7877
  name,
7657
7878
  provider,
@@ -7696,7 +7917,7 @@ function emitPlatformResourceEdge(graph, anchorId, edgeType, kind, name, provide
7696
7917
  source: anchorId,
7697
7918
  target: node.id,
7698
7919
  type: edgeType,
7699
- provenance: Provenance14.EXTRACTED,
7920
+ provenance: Provenance15.EXTRACTED,
7700
7921
  confidence: confidenceForExtracted12("structural"),
7701
7922
  evidence: { file: evidenceFile, ...line !== void 0 ? { line } : {} }
7702
7923
  };
@@ -7714,7 +7935,7 @@ function dependsOnList(value) {
7714
7935
  }
7715
7936
  function serviceNameToServiceNode(name, services) {
7716
7937
  for (const s of services) {
7717
- if (s.node.name === name || path38.basename(s.dir) === name) return s.node.id;
7938
+ if (s.node.name === name || path39.basename(s.dir) === name) return s.node.id;
7718
7939
  }
7719
7940
  return null;
7720
7941
  }
@@ -7723,7 +7944,7 @@ async function addComposeInfra(graph, scanPath, services) {
7723
7944
  let edgesAdded = 0;
7724
7945
  let composePath = null;
7725
7946
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
7726
- const abs = path38.join(scanPath, name);
7947
+ const abs = path39.join(scanPath, name);
7727
7948
  if (await exists(abs)) {
7728
7949
  composePath = abs;
7729
7950
  break;
@@ -7736,13 +7957,13 @@ async function addComposeInfra(graph, scanPath, services) {
7736
7957
  } catch (err) {
7737
7958
  recordExtractionError(
7738
7959
  "infra docker-compose",
7739
- path38.relative(scanPath, composePath),
7960
+ path39.relative(scanPath, composePath),
7740
7961
  err
7741
7962
  );
7742
7963
  return { nodesAdded, edgesAdded };
7743
7964
  }
7744
7965
  if (!compose?.services) return { nodesAdded, edgesAdded };
7745
- const evidenceFile = path38.relative(scanPath, composePath).split(path38.sep).join("/");
7966
+ const evidenceFile = path39.relative(scanPath, composePath).split(path39.sep).join("/");
7746
7967
  const composeNameToNodeId = /* @__PURE__ */ new Map();
7747
7968
  for (const [composeName, svc] of Object.entries(compose.services)) {
7748
7969
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -7771,7 +7992,7 @@ async function addComposeInfra(graph, scanPath, services) {
7771
7992
  source: sourceId,
7772
7993
  target: targetId,
7773
7994
  type: EdgeType15.DEPENDS_ON,
7774
- provenance: Provenance15.EXTRACTED,
7995
+ provenance: Provenance16.EXTRACTED,
7775
7996
  confidence: confidenceForExtracted13("structural"),
7776
7997
  evidence: { file: evidenceFile }
7777
7998
  };
@@ -7783,9 +8004,9 @@ async function addComposeInfra(graph, scanPath, services) {
7783
8004
  }
7784
8005
 
7785
8006
  // src/extract/infra/dockerfile.ts
7786
- import path39 from "path";
8007
+ import path40 from "path";
7787
8008
  import { promises as fs17 } from "fs";
7788
- import { EdgeType as EdgeType16, Provenance as Provenance16, confidenceForExtracted as confidenceForExtracted14 } from "@neat.is/types";
8009
+ import { EdgeType as EdgeType16, Provenance as Provenance17, confidenceForExtracted as confidenceForExtracted14 } from "@neat.is/types";
7789
8010
  function readDockerfile(content) {
7790
8011
  let image = null;
7791
8012
  const ports = [];
@@ -7814,7 +8035,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
7814
8035
  let nodesAdded = 0;
7815
8036
  let edgesAdded = 0;
7816
8037
  for (const service of services) {
7817
- const dockerfilePath = path39.join(service.dir, "Dockerfile");
8038
+ const dockerfilePath = path40.join(service.dir, "Dockerfile");
7818
8039
  if (!await exists(dockerfilePath)) continue;
7819
8040
  let content;
7820
8041
  try {
@@ -7822,7 +8043,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
7822
8043
  } catch (err) {
7823
8044
  recordExtractionError(
7824
8045
  "infra dockerfile",
7825
- path39.relative(scanPath, dockerfilePath),
8046
+ path40.relative(scanPath, dockerfilePath),
7826
8047
  err
7827
8048
  );
7828
8049
  continue;
@@ -7834,8 +8055,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
7834
8055
  graph.addNode(node.id, node);
7835
8056
  nodesAdded++;
7836
8057
  }
7837
- const relDockerfile = toPosix(path39.relative(service.dir, dockerfilePath));
7838
- const evidenceFile = toPosix(path39.relative(scanPath, dockerfilePath));
8058
+ const relDockerfile = toPosix(path40.relative(service.dir, dockerfilePath));
8059
+ const evidenceFile = toPosix(path40.relative(scanPath, dockerfilePath));
7839
8060
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
7840
8061
  graph,
7841
8062
  service.pkg.name,
@@ -7851,7 +8072,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
7851
8072
  source: fileNodeId,
7852
8073
  target: node.id,
7853
8074
  type: EdgeType16.RUNS_ON,
7854
- provenance: Provenance16.EXTRACTED,
8075
+ provenance: Provenance17.EXTRACTED,
7855
8076
  confidence: confidenceForExtracted14("structural"),
7856
8077
  evidence: {
7857
8078
  file: evidenceFile,
@@ -7874,7 +8095,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
7874
8095
  source: fileNodeId,
7875
8096
  target: portNode.id,
7876
8097
  type: EdgeType16.CONNECTS_TO,
7877
- provenance: Provenance16.EXTRACTED,
8098
+ provenance: Provenance17.EXTRACTED,
7878
8099
  confidence: confidenceForExtracted14("structural"),
7879
8100
  evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
7880
8101
  };
@@ -7887,8 +8108,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
7887
8108
 
7888
8109
  // src/extract/infra/terraform.ts
7889
8110
  import { promises as fs18 } from "fs";
7890
- import path40 from "path";
7891
- import { EdgeType as EdgeType17, Provenance as Provenance17, confidenceForExtracted as confidenceForExtracted15 } from "@neat.is/types";
8111
+ import path41 from "path";
8112
+ import { EdgeType as EdgeType17, Provenance as Provenance18, confidenceForExtracted as confidenceForExtracted15 } from "@neat.is/types";
7892
8113
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
7893
8114
  var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
7894
8115
  async function walkTfFiles(start, depth = 0, max = 5) {
@@ -7898,11 +8119,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
7898
8119
  for (const entry of entries) {
7899
8120
  if (entry.isDirectory()) {
7900
8121
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
7901
- const child = path40.join(start, entry.name);
8122
+ const child = path41.join(start, entry.name);
7902
8123
  if (await isPythonVenvDir(child)) continue;
7903
8124
  out.push(...await walkTfFiles(child, depth + 1, max));
7904
8125
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
7905
- out.push(path40.join(start, entry.name));
8126
+ out.push(path41.join(start, entry.name));
7906
8127
  }
7907
8128
  }
7908
8129
  return out;
@@ -7934,7 +8155,7 @@ async function addTerraformResources(graph, scanPath) {
7934
8155
  const files = await walkTfFiles(scanPath);
7935
8156
  for (const file of files) {
7936
8157
  const content = await fs18.readFile(file, "utf8");
7937
- const evidenceFile = toPosix(path40.relative(scanPath, file));
8158
+ const evidenceFile = toPosix(path41.relative(scanPath, file));
7938
8159
  const resources = [];
7939
8160
  const byKey = /* @__PURE__ */ new Map();
7940
8161
  RESOURCE_RE.lastIndex = 0;
@@ -7977,7 +8198,7 @@ async function addTerraformResources(graph, scanPath) {
7977
8198
  source: resource.nodeId,
7978
8199
  target: target.nodeId,
7979
8200
  type: EdgeType17.DEPENDS_ON,
7980
- provenance: Provenance17.EXTRACTED,
8201
+ provenance: Provenance18.EXTRACTED,
7981
8202
  confidence: confidenceForExtracted15("structural"),
7982
8203
  evidence: { file: evidenceFile, line, snippet: key }
7983
8204
  };
@@ -7991,7 +8212,7 @@ async function addTerraformResources(graph, scanPath) {
7991
8212
 
7992
8213
  // src/extract/infra/k8s.ts
7993
8214
  import { promises as fs19 } from "fs";
7994
- import path41 from "path";
8215
+ import path42 from "path";
7995
8216
  import { parseAllDocuments as parseAllDocuments2 } from "yaml";
7996
8217
  var K8S_KIND_TO_INFRA_KIND = {
7997
8218
  Service: "k8s-service",
@@ -8009,11 +8230,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
8009
8230
  for (const entry of entries) {
8010
8231
  if (entry.isDirectory()) {
8011
8232
  if (IGNORED_DIRS.has(entry.name)) continue;
8012
- const child = path41.join(start, entry.name);
8233
+ const child = path42.join(start, entry.name);
8013
8234
  if (await isPythonVenvDir(child)) continue;
8014
8235
  out.push(...await walkYamlFiles2(child, depth + 1, max));
8015
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path41.extname(entry.name))) {
8016
- out.push(path41.join(start, entry.name));
8236
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path42.extname(entry.name))) {
8237
+ out.push(path42.join(start, entry.name));
8017
8238
  }
8018
8239
  }
8019
8240
  return out;
@@ -8046,13 +8267,13 @@ async function addK8sResources(graph, scanPath) {
8046
8267
 
8047
8268
  // src/extract/infra/cloudflare.ts
8048
8269
  import { promises as fs20 } from "fs";
8049
- import path42 from "path";
8270
+ import path43 from "path";
8050
8271
  import { parse as parseToml2 } from "smol-toml";
8051
- import { EdgeType as EdgeType18, Provenance as Provenance18, confidenceForExtracted as confidenceForExtracted16 } from "@neat.is/types";
8272
+ import { EdgeType as EdgeType18, Provenance as Provenance19, confidenceForExtracted as confidenceForExtracted16 } from "@neat.is/types";
8052
8273
  var WRANGLER_FILENAMES = ["wrangler.toml", "wrangler.jsonc", "wrangler.json"];
8053
8274
  async function readWranglerConfig(dir) {
8054
8275
  for (const filename of WRANGLER_FILENAMES) {
8055
- const abs = path42.join(dir, filename);
8276
+ const abs = path43.join(dir, filename);
8056
8277
  if (!await exists(abs)) continue;
8057
8278
  const raw = await fs20.readFile(abs, "utf8");
8058
8279
  const config = filename === "wrangler.toml" ? parseToml2(raw) : JSON.parse(maskCommentsInSource(raw));
@@ -8096,7 +8317,7 @@ function addResourceEdge(graph, anchorId, edgeType, kind, name, evidenceFile, li
8096
8317
  source: anchorId,
8097
8318
  target: node.id,
8098
8319
  type: edgeType,
8099
- provenance: Provenance18.EXTRACTED,
8320
+ provenance: Provenance19.EXTRACTED,
8100
8321
  confidence: confidenceForExtracted16("structural"),
8101
8322
  evidence: { file: evidenceFile, ...line !== void 0 ? { line } : {} }
8102
8323
  };
@@ -8115,11 +8336,11 @@ async function addCloudflareWorkers(graph, services, scanPath) {
8115
8336
  try {
8116
8337
  read = await readWranglerConfig(service.dir);
8117
8338
  } catch (err) {
8118
- recordExtractionError("infra cloudflare", path42.relative(scanPath, service.dir), err);
8339
+ recordExtractionError("infra cloudflare", path43.relative(scanPath, service.dir), err);
8119
8340
  continue;
8120
8341
  }
8121
8342
  if (!read || !read.config.name) continue;
8122
- const evidenceFile = toPosix(path42.relative(scanPath, path42.join(service.dir, read.relFile)));
8343
+ const evidenceFile = toPosix(path43.relative(scanPath, path43.join(service.dir, read.relFile)));
8123
8344
  discovered.push({ service, config: read.config, relFile: read.relFile, raw: read.raw, evidenceFile });
8124
8345
  }
8125
8346
  for (const worker of discovered) {
@@ -8131,7 +8352,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
8131
8352
  }
8132
8353
  let anchorId = service.node.id;
8133
8354
  if (config.main) {
8134
- const entryRelPath = toPosix(path42.normalize(config.main));
8355
+ const entryRelPath = toPosix(path43.normalize(config.main));
8135
8356
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
8136
8357
  graph,
8137
8358
  service.pkg.name,
@@ -8165,7 +8386,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
8165
8386
  source: anchorId,
8166
8387
  target: runtimeNode.id,
8167
8388
  type: EdgeType18.RUNS_ON,
8168
- provenance: Provenance18.EXTRACTED,
8389
+ provenance: Provenance19.EXTRACTED,
8169
8390
  confidence: confidenceForExtracted16("structural"),
8170
8391
  evidence: {
8171
8392
  file: evidenceFile,
@@ -8251,7 +8472,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
8251
8472
  source: anchorId,
8252
8473
  target: target.anchorId,
8253
8474
  type: EdgeType18.CALLS,
8254
- provenance: Provenance18.EXTRACTED,
8475
+ provenance: Provenance19.EXTRACTED,
8255
8476
  confidence: confidenceForExtracted16("structural"),
8256
8477
  evidence: { file: evidenceFile, line: lineContaining2(raw, svc.service) }
8257
8478
  };
@@ -8278,12 +8499,12 @@ async function addCloudflareWorkers(graph, services, scanPath) {
8278
8499
 
8279
8500
  // src/extract/infra/vercel.ts
8280
8501
  import { promises as fs21 } from "fs";
8281
- import path43 from "path";
8502
+ import path44 from "path";
8282
8503
  import { EdgeType as EdgeType19 } from "@neat.is/types";
8283
8504
  var VERCEL_CONFIG_FILENAMES = ["vercel.json", "vercel.jsonc"];
8284
8505
  async function readVercelConfig(dir) {
8285
8506
  for (const filename of VERCEL_CONFIG_FILENAMES) {
8286
- const abs = path43.join(dir, filename);
8507
+ const abs = path44.join(dir, filename);
8287
8508
  if (!await exists(abs)) continue;
8288
8509
  const raw = await fs21.readFile(abs, "utf8");
8289
8510
  const config = JSON.parse(maskCommentsInSource(raw));
@@ -8292,7 +8513,7 @@ async function readVercelConfig(dir) {
8292
8513
  return null;
8293
8514
  }
8294
8515
  async function readLinkedProjectName(dir) {
8295
- const abs = path43.join(dir, ".vercel", "project.json");
8516
+ const abs = path44.join(dir, ".vercel", "project.json");
8296
8517
  if (!await exists(abs)) return void 0;
8297
8518
  const parsed = JSON.parse(await fs21.readFile(abs, "utf8"));
8298
8519
  return typeof parsed.projectName === "string" ? parsed.projectName : void 0;
@@ -8310,7 +8531,7 @@ async function addVercelServices(graph, services, scanPath) {
8310
8531
  read = await readVercelConfig(service.dir);
8311
8532
  projectName = await readLinkedProjectName(service.dir);
8312
8533
  } catch (err) {
8313
- recordExtractionError("infra vercel", path43.relative(scanPath, service.dir), err);
8534
+ recordExtractionError("infra vercel", path44.relative(scanPath, service.dir), err);
8314
8535
  continue;
8315
8536
  }
8316
8537
  if (!read && !projectName) continue;
@@ -8326,7 +8547,7 @@ async function addVercelServices(graph, services, scanPath) {
8326
8547
  const anchorId = service.node.id;
8327
8548
  if (!read) continue;
8328
8549
  const { config, relFile, raw } = read;
8329
- const evidenceFile = toPosix(path43.relative(scanPath, path43.join(service.dir, relFile)));
8550
+ const evidenceFile = toPosix(path44.relative(scanPath, path44.join(service.dir, relFile)));
8330
8551
  const add = (edgeType, kind, name) => {
8331
8552
  if (!name) return;
8332
8553
  const result = emitPlatformResourceEdge(
@@ -8355,13 +8576,13 @@ async function addVercelServices(graph, services, scanPath) {
8355
8576
 
8356
8577
  // src/extract/infra/railway.ts
8357
8578
  import { promises as fs22 } from "fs";
8358
- import path44 from "path";
8579
+ import path45 from "path";
8359
8580
  import { parse as parseToml3 } from "smol-toml";
8360
8581
  import { EdgeType as EdgeType20 } from "@neat.is/types";
8361
8582
  var RAILWAY_FILENAMES = ["railway.toml", "railway.json", "railway.jsonc"];
8362
8583
  async function readRailwayConfig(dir) {
8363
8584
  for (const filename of RAILWAY_FILENAMES) {
8364
- const abs = path44.join(dir, filename);
8585
+ const abs = path45.join(dir, filename);
8365
8586
  if (!await exists(abs)) continue;
8366
8587
  const raw = await fs22.readFile(abs, "utf8");
8367
8588
  const config = filename === "railway.toml" ? parseToml3(raw) : JSON.parse(maskCommentsInSource(raw));
@@ -8377,7 +8598,7 @@ async function addRailwayServices(graph, services, scanPath) {
8377
8598
  try {
8378
8599
  read = await readRailwayConfig(service.dir);
8379
8600
  } catch (err) {
8380
- recordExtractionError("infra railway", path44.relative(scanPath, service.dir), err);
8601
+ recordExtractionError("infra railway", path45.relative(scanPath, service.dir), err);
8381
8602
  continue;
8382
8603
  }
8383
8604
  if (!read) continue;
@@ -8387,7 +8608,7 @@ async function addRailwayServices(graph, services, scanPath) {
8387
8608
  }
8388
8609
  const anchorId = service.node.id;
8389
8610
  const { config, relFile, raw } = read;
8390
- const evidenceFile = toPosix(path44.relative(scanPath, path44.join(service.dir, relFile)));
8611
+ const evidenceFile = toPosix(path45.relative(scanPath, path45.join(service.dir, relFile)));
8391
8612
  const add = (edgeType, kind, name) => {
8392
8613
  if (!name) return;
8393
8614
  const result = emitPlatformResourceEdge(
@@ -8412,12 +8633,12 @@ async function addRailwayServices(graph, services, scanPath) {
8412
8633
 
8413
8634
  // src/extract/infra/supabase.ts
8414
8635
  import { promises as fs23 } from "fs";
8415
- import path45 from "path";
8636
+ import path46 from "path";
8416
8637
  import { parse as parseToml4 } from "smol-toml";
8417
8638
  import { EdgeType as EdgeType21 } from "@neat.is/types";
8418
8639
  async function readSupabaseConfig(dir) {
8419
- const relFile = path45.join("supabase", "config.toml");
8420
- const abs = path45.join(dir, relFile);
8640
+ const relFile = path46.join("supabase", "config.toml");
8641
+ const abs = path46.join(dir, relFile);
8421
8642
  if (!await exists(abs)) return null;
8422
8643
  const raw = await fs23.readFile(abs, "utf8");
8423
8644
  const config = parseToml4(raw);
@@ -8431,7 +8652,7 @@ async function addSupabaseProjects(graph, services, scanPath) {
8431
8652
  try {
8432
8653
  read = await readSupabaseConfig(service.dir);
8433
8654
  } catch (err) {
8434
- recordExtractionError("infra supabase", path45.relative(scanPath, service.dir), err);
8655
+ recordExtractionError("infra supabase", path46.relative(scanPath, service.dir), err);
8435
8656
  continue;
8436
8657
  }
8437
8658
  if (!read) continue;
@@ -8446,7 +8667,7 @@ async function addSupabaseProjects(graph, services, scanPath) {
8446
8667
  });
8447
8668
  }
8448
8669
  const anchorId = service.node.id;
8449
- const evidenceFile = toPosix(path45.relative(scanPath, path45.join(service.dir, relFile)));
8670
+ const evidenceFile = toPosix(path46.relative(scanPath, path46.join(service.dir, relFile)));
8450
8671
  const add = (edgeType, kind, name) => {
8451
8672
  if (!name) return;
8452
8673
  const result = emitPlatformResourceEdge(
@@ -8487,12 +8708,12 @@ async function addInfra(graph, scanPath, services) {
8487
8708
  }
8488
8709
 
8489
8710
  // src/extract/index.ts
8490
- import path47 from "path";
8711
+ import path48 from "path";
8491
8712
 
8492
8713
  // src/extract/retire.ts
8493
8714
  import { existsSync as existsSync2 } from "fs";
8494
- import path46 from "path";
8495
- import { NodeType as NodeType17, Provenance as Provenance19 } from "@neat.is/types";
8715
+ import path47 from "path";
8716
+ import { NodeType as NodeType17, Provenance as Provenance20 } from "@neat.is/types";
8496
8717
  function dropOrphanedFileNodes(graph) {
8497
8718
  const orphans = [];
8498
8719
  graph.forEachNode((id, attrs) => {
@@ -8509,7 +8730,7 @@ function retireEdgesByFile(graph, file) {
8509
8730
  const toDrop = [];
8510
8731
  graph.forEachEdge((id, attrs) => {
8511
8732
  const edge = attrs;
8512
- if (edge.provenance !== Provenance19.EXTRACTED) return;
8733
+ if (edge.provenance !== Provenance20.EXTRACTED) return;
8513
8734
  if (!edge.evidence?.file) return;
8514
8735
  if (edge.evidence.file === normalized) toDrop.push(id);
8515
8736
  });
@@ -8522,14 +8743,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
8522
8743
  const bases = [scanPath, ...serviceDirs];
8523
8744
  graph.forEachEdge((id, attrs) => {
8524
8745
  const edge = attrs;
8525
- if (edge.provenance !== Provenance19.EXTRACTED) return;
8746
+ if (edge.provenance !== Provenance20.EXTRACTED) return;
8526
8747
  const evidenceFile = edge.evidence?.file;
8527
8748
  if (!evidenceFile) return;
8528
- if (path46.isAbsolute(evidenceFile)) {
8749
+ if (path47.isAbsolute(evidenceFile)) {
8529
8750
  if (!existsSync2(evidenceFile)) toDrop.push(id);
8530
8751
  return;
8531
8752
  }
8532
- const found = bases.some((base) => existsSync2(path46.join(base, evidenceFile)));
8753
+ const found = bases.some((base) => existsSync2(path47.join(base, evidenceFile)));
8533
8754
  if (!found) toDrop.push(id);
8534
8755
  });
8535
8756
  for (const id of toDrop) graph.dropEdge(id);
@@ -8582,7 +8803,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
8582
8803
  }
8583
8804
  const droppedEntries = drainDroppedExtracted();
8584
8805
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
8585
- const rejectedPath = path47.join(path47.dirname(opts.errorsPath), "rejected.ndjson");
8806
+ const rejectedPath = path48.join(path48.dirname(opts.errorsPath), "rejected.ndjson");
8586
8807
  try {
8587
8808
  await writeRejectedExtracted(droppedEntries, rejectedPath);
8588
8809
  } catch (err) {
@@ -8622,7 +8843,7 @@ import {
8622
8843
  NodeType as NodeType18,
8623
8844
  parseEdgeId,
8624
8845
  parseFileId,
8625
- Provenance as Provenance20,
8846
+ Provenance as Provenance21,
8626
8847
  serviceId as serviceId5
8627
8848
  } from "@neat.is/types";
8628
8849
  function bucketKey(source, target, type) {
@@ -8646,17 +8867,17 @@ function bucketEdges(graph) {
8646
8867
  const key = bucketKey(source, e.target, e.type);
8647
8868
  const cur = buckets2.get(key) ?? { source, target: e.target, type: e.type };
8648
8869
  switch (provenance) {
8649
- case Provenance20.EXTRACTED:
8870
+ case Provenance21.EXTRACTED:
8650
8871
  cur.extracted = e;
8651
8872
  break;
8652
- case Provenance20.OBSERVED:
8873
+ case Provenance21.OBSERVED:
8653
8874
  cur.observed = e;
8654
8875
  break;
8655
- case Provenance20.INFERRED:
8876
+ case Provenance21.INFERRED:
8656
8877
  cur.inferred = e;
8657
8878
  break;
8658
8879
  default:
8659
- if (e.provenance === Provenance20.STALE) cur.stale = e;
8880
+ if (e.provenance === Provenance21.STALE) cur.stale = e;
8660
8881
  }
8661
8882
  buckets2.set(key, cur);
8662
8883
  });
@@ -8744,7 +8965,7 @@ function declaredHostFor(svc) {
8744
8965
  function hasExtractedConfiguredBy(graph, svcId) {
8745
8966
  for (const edgeId of graph.outboundEdges(svcId)) {
8746
8967
  const e = graph.getEdgeAttributes(edgeId);
8747
- if (e.type === EdgeType22.CONFIGURED_BY && e.provenance === Provenance20.EXTRACTED) {
8968
+ if (e.type === EdgeType22.CONFIGURED_BY && e.provenance === Provenance21.EXTRACTED) {
8748
8969
  return true;
8749
8970
  }
8750
8971
  }
@@ -8758,7 +8979,7 @@ function detectHostMismatch(graph, svcId, svc) {
8758
8979
  for (const edgeId of graph.outboundEdges(svcId)) {
8759
8980
  const edge = graph.getEdgeAttributes(edgeId);
8760
8981
  if (edge.type !== EdgeType22.CONNECTS_TO) continue;
8761
- if (edge.provenance !== Provenance20.OBSERVED) continue;
8982
+ if (edge.provenance !== Provenance21.OBSERVED) continue;
8762
8983
  const target = graph.getNodeAttributes(edge.target);
8763
8984
  if (target.type !== NodeType18.DatabaseNode) continue;
8764
8985
  const observedHost = target.host?.trim();
@@ -8783,7 +9004,7 @@ function detectCompatDivergences(graph, svcId, svc) {
8783
9004
  for (const edgeId of graph.outboundEdges(svcId)) {
8784
9005
  const edge = graph.getEdgeAttributes(edgeId);
8785
9006
  if (edge.type !== EdgeType22.CONNECTS_TO) continue;
8786
- if (edge.provenance !== Provenance20.OBSERVED) continue;
9007
+ if (edge.provenance !== Provenance21.OBSERVED) continue;
8787
9008
  const target = graph.getNodeAttributes(edge.target);
8788
9009
  if (target.type !== NodeType18.DatabaseNode) continue;
8789
9010
  for (const pair of compatPairs()) {
@@ -8835,6 +9056,44 @@ function detectCompatDivergences(graph, svcId, svc) {
8835
9056
  }
8836
9057
  return out;
8837
9058
  }
9059
+ var RECOMMENDATION_COLUMN_MISSING_OBSERVED = "Verify the column is exercised in production; a migration that renamed or dropped it may have left a writer declaring the old name.";
9060
+ var RECOMMENDATION_COLUMN_MISSING_EXTRACTED = "The schema or migration is likely behind the code \u2014 production writes a column the declared schema does not carry. Check for a field rename that updated the query but not the model.";
9061
+ function detectColumnDrift(node) {
9062
+ const columns = node.columns;
9063
+ if (!columns || columns.length === 0) return [];
9064
+ const anyDeclared = columns.some(columnIsDeclared);
9065
+ const anyObserved = columns.some(columnIsObserved);
9066
+ if (!anyDeclared || !anyObserved) return [];
9067
+ const out = [];
9068
+ for (const col of columns) {
9069
+ const declared = columnIsDeclared(col);
9070
+ const observed = columnIsObserved(col);
9071
+ if (declared && !observed) {
9072
+ out.push({
9073
+ type: "missing-observed",
9074
+ source: node.id,
9075
+ target: node.id,
9076
+ table: node.id,
9077
+ column: col.name,
9078
+ confidence: clampConfidence(col.confidence),
9079
+ reason: `Schema declares column ${node.name}.${col.name} but no production statement has touched it.`,
9080
+ recommendation: RECOMMENDATION_COLUMN_MISSING_OBSERVED
9081
+ });
9082
+ } else if (observed && !declared) {
9083
+ out.push({
9084
+ type: "missing-extracted",
9085
+ source: node.id,
9086
+ target: node.id,
9087
+ table: node.id,
9088
+ column: col.name,
9089
+ confidence: clampConfidence(col.confidence),
9090
+ reason: `Production touched column ${node.name}.${col.name} but the schema does not declare it.`,
9091
+ recommendation: RECOMMENDATION_COLUMN_MISSING_EXTRACTED
9092
+ });
9093
+ }
9094
+ }
9095
+ return out;
9096
+ }
8838
9097
  function involvesNode(d, nodeId) {
8839
9098
  return d.source === nodeId || d.target === nodeId;
8840
9099
  }
@@ -8863,10 +9122,15 @@ function computeDivergences(graph, opts = {}) {
8863
9122
  }
8864
9123
  graph.forEachNode((nodeId, attrs) => {
8865
9124
  const n = attrs;
8866
- if (n.type !== NodeType18.ServiceNode) return;
8867
- const svc = n;
8868
- for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
8869
- for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
9125
+ if (n.type === NodeType18.ServiceNode) {
9126
+ const svc = n;
9127
+ for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
9128
+ for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
9129
+ return;
9130
+ }
9131
+ if (n.type === NodeType18.InfraNode && n.kind === "sql-table") {
9132
+ for (const d of detectColumnDrift(n)) all.push(d);
9133
+ }
8870
9134
  });
8871
9135
  const reconciled = suppressHostMismatchHalves(all);
8872
9136
  let filtered = reconciled;
@@ -8895,7 +9159,10 @@ function computeDivergences(graph, opts = {}) {
8895
9159
  if (lead !== 0) return lead;
8896
9160
  if (a.type !== b.type) return a.type.localeCompare(b.type);
8897
9161
  if (a.source !== b.source) return a.source.localeCompare(b.source);
8898
- return a.target.localeCompare(b.target);
9162
+ if (a.target !== b.target) return a.target.localeCompare(b.target);
9163
+ const ac = "column" in a && a.column ? a.column : "";
9164
+ const bc = "column" in b && b.column ? b.column : "";
9165
+ return ac.localeCompare(bc);
8899
9166
  });
8900
9167
  return DivergenceResultSchema.parse({
8901
9168
  divergences: filtered,
@@ -8906,9 +9173,9 @@ function computeDivergences(graph, opts = {}) {
8906
9173
 
8907
9174
  // src/persist.ts
8908
9175
  import { promises as fs24 } from "fs";
8909
- import path48 from "path";
8910
- import { Provenance as Provenance21, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
8911
- var SCHEMA_VERSION = 5;
9176
+ import path49 from "path";
9177
+ import { NodeType as NodeType19, Provenance as Provenance22, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
9178
+ var SCHEMA_VERSION = 6;
8912
9179
  function migrateV1ToV2(payload) {
8913
9180
  const nodes = payload.graph.nodes;
8914
9181
  if (Array.isArray(nodes)) {
@@ -8926,13 +9193,25 @@ function migrateV3ToV4(payload) {
8926
9193
  function migrateV4ToV5(payload) {
8927
9194
  return { ...payload, schemaVersion: 5 };
8928
9195
  }
9196
+ function migrateV5ToV6(payload) {
9197
+ const nodes = payload.graph.nodes;
9198
+ if (Array.isArray(nodes)) {
9199
+ for (const node of nodes) {
9200
+ const attrs = node.attributes;
9201
+ if (!attrs || attrs.type !== NodeType19.InfraNode) continue;
9202
+ if (attrs.kind !== "sql-table" && attrs.kind !== "supabase-table") continue;
9203
+ if (!Array.isArray(attrs.columns)) attrs.columns = [];
9204
+ }
9205
+ }
9206
+ return { ...payload, schemaVersion: 6 };
9207
+ }
8929
9208
  function migrateV2ToV3(payload) {
8930
9209
  const edges = payload.graph.edges;
8931
9210
  if (Array.isArray(edges)) {
8932
9211
  for (const edge of edges) {
8933
9212
  const attrs = edge.attributes;
8934
9213
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
8935
- attrs.provenance = Provenance21.OBSERVED;
9214
+ attrs.provenance = Provenance22.OBSERVED;
8936
9215
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
8937
9216
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
8938
9217
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
@@ -8946,7 +9225,7 @@ function migrateV2ToV3(payload) {
8946
9225
  return { ...payload, schemaVersion: 3 };
8947
9226
  }
8948
9227
  async function ensureDir(filePath) {
8949
- await fs24.mkdir(path48.dirname(filePath), { recursive: true });
9228
+ await fs24.mkdir(path49.dirname(filePath), { recursive: true });
8950
9229
  }
8951
9230
  async function saveGraphToDisk(graph, outPath) {
8952
9231
  await ensureDir(outPath);
@@ -8980,6 +9259,9 @@ async function loadGraphFromDisk(graph, outPath) {
8980
9259
  if (payload.schemaVersion === 4) {
8981
9260
  payload = migrateV4ToV5(payload);
8982
9261
  }
9262
+ if (payload.schemaVersion === 5) {
9263
+ payload = migrateV5ToV6(payload);
9264
+ }
8983
9265
  if (payload.schemaVersion !== SCHEMA_VERSION) {
8984
9266
  throw new Error(
8985
9267
  `persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
@@ -9105,23 +9387,23 @@ function canonicalJson(value) {
9105
9387
  }
9106
9388
 
9107
9389
  // src/projects.ts
9108
- import path49 from "path";
9390
+ import path50 from "path";
9109
9391
  function pathsForProject(project, baseDir) {
9110
9392
  if (project === DEFAULT_PROJECT) {
9111
9393
  return {
9112
- snapshotPath: path49.join(baseDir, "graph.json"),
9113
- errorsPath: path49.join(baseDir, "errors.ndjson"),
9114
- staleEventsPath: path49.join(baseDir, "stale-events.ndjson"),
9115
- embeddingsCachePath: path49.join(baseDir, "embeddings.json"),
9116
- policyViolationsPath: path49.join(baseDir, "policy-violations.ndjson")
9394
+ snapshotPath: path50.join(baseDir, "graph.json"),
9395
+ errorsPath: path50.join(baseDir, "errors.ndjson"),
9396
+ staleEventsPath: path50.join(baseDir, "stale-events.ndjson"),
9397
+ embeddingsCachePath: path50.join(baseDir, "embeddings.json"),
9398
+ policyViolationsPath: path50.join(baseDir, "policy-violations.ndjson")
9117
9399
  };
9118
9400
  }
9119
9401
  return {
9120
- snapshotPath: path49.join(baseDir, `${project}.json`),
9121
- errorsPath: path49.join(baseDir, `errors.${project}.ndjson`),
9122
- staleEventsPath: path49.join(baseDir, `stale-events.${project}.ndjson`),
9123
- embeddingsCachePath: path49.join(baseDir, `embeddings.${project}.json`),
9124
- policyViolationsPath: path49.join(baseDir, `policy-violations.${project}.ndjson`)
9402
+ snapshotPath: path50.join(baseDir, `${project}.json`),
9403
+ errorsPath: path50.join(baseDir, `errors.${project}.ndjson`),
9404
+ staleEventsPath: path50.join(baseDir, `stale-events.${project}.ndjson`),
9405
+ embeddingsCachePath: path50.join(baseDir, `embeddings.${project}.json`),
9406
+ policyViolationsPath: path50.join(baseDir, `policy-violations.${project}.ndjson`)
9125
9407
  };
9126
9408
  }
9127
9409
  var Projects = class {
@@ -9162,7 +9444,7 @@ function parseExtraProjects(raw) {
9162
9444
  // src/registry.ts
9163
9445
  import { promises as fs26 } from "fs";
9164
9446
  import os2 from "os";
9165
- import path50 from "path";
9447
+ import path51 from "path";
9166
9448
  import {
9167
9449
  RegistryFileSchema
9168
9450
  } from "@neat.is/types";
@@ -9170,20 +9452,20 @@ var LOCK_TIMEOUT_MS = 5e3;
9170
9452
  var LOCK_RETRY_MS = 50;
9171
9453
  function neatHome() {
9172
9454
  const override = process.env.NEAT_HOME;
9173
- if (override && override.length > 0) return path50.resolve(override);
9174
- return path50.join(os2.homedir(), ".neat");
9455
+ if (override && override.length > 0) return path51.resolve(override);
9456
+ return path51.join(os2.homedir(), ".neat");
9175
9457
  }
9176
9458
  function registryPath() {
9177
- return path50.join(neatHome(), "projects.json");
9459
+ return path51.join(neatHome(), "projects.json");
9178
9460
  }
9179
9461
  function registryLockPath() {
9180
- return path50.join(neatHome(), "projects.json.lock");
9462
+ return path51.join(neatHome(), "projects.json.lock");
9181
9463
  }
9182
9464
  function daemonPidPath() {
9183
- return path50.join(neatHome(), "neatd.pid");
9465
+ return path51.join(neatHome(), "neatd.pid");
9184
9466
  }
9185
9467
  function daemonsDir() {
9186
- return path50.join(neatHome(), "daemons");
9468
+ return path51.join(neatHome(), "daemons");
9187
9469
  }
9188
9470
  function isFiniteInt(v) {
9189
9471
  return typeof v === "number" && Number.isFinite(v);
@@ -9224,7 +9506,7 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
9224
9506
  const out = [];
9225
9507
  for (const name of names) {
9226
9508
  if (!name.endsWith(".json")) continue;
9227
- const file = path50.join(dir, name);
9509
+ const file = path51.join(dir, name);
9228
9510
  let raw;
9229
9511
  try {
9230
9512
  raw = await fs26.readFile(file, "utf8");
@@ -9345,7 +9627,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
9345
9627
  }
9346
9628
  }
9347
9629
  async function normalizeProjectPath(input) {
9348
- const resolved = path50.resolve(input);
9630
+ const resolved = path51.resolve(input);
9349
9631
  try {
9350
9632
  return await fs26.realpath(resolved);
9351
9633
  } catch {
@@ -9353,7 +9635,7 @@ async function normalizeProjectPath(input) {
9353
9635
  }
9354
9636
  }
9355
9637
  async function writeAtomically(target, contents) {
9356
- await fs26.mkdir(path50.dirname(target), { recursive: true });
9638
+ await fs26.mkdir(path51.dirname(target), { recursive: true });
9357
9639
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
9358
9640
  const fd = await fs26.open(tmp, "w");
9359
9641
  try {
@@ -9366,7 +9648,7 @@ async function writeAtomically(target, contents) {
9366
9648
  }
9367
9649
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
9368
9650
  const deadline = Date.now() + timeoutMs;
9369
- await fs26.mkdir(path50.dirname(lockPath), { recursive: true });
9651
+ await fs26.mkdir(path51.dirname(lockPath), { recursive: true });
9370
9652
  let probedHolder = false;
9371
9653
  while (true) {
9372
9654
  try {
@@ -9562,13 +9844,13 @@ import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } f
9562
9844
 
9563
9845
  // src/extend/index.ts
9564
9846
  import { promises as fs28 } from "fs";
9565
- import path52 from "path";
9847
+ import path53 from "path";
9566
9848
  import os3 from "os";
9567
9849
  import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
9568
9850
 
9569
9851
  // src/installers/package-manager.ts
9570
9852
  import { promises as fs27 } from "fs";
9571
- import path51 from "path";
9853
+ import path52 from "path";
9572
9854
  import { spawn } from "child_process";
9573
9855
  var LOCKFILE_PRIORITY = [
9574
9856
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -9590,22 +9872,22 @@ async function exists2(p) {
9590
9872
  }
9591
9873
  }
9592
9874
  async function detectPackageManager(serviceDir) {
9593
- let dir = path51.resolve(serviceDir);
9875
+ let dir = path52.resolve(serviceDir);
9594
9876
  const stops = /* @__PURE__ */ new Set();
9595
9877
  for (let i = 0; i < 64; i++) {
9596
9878
  if (stops.has(dir)) break;
9597
9879
  stops.add(dir);
9598
9880
  for (const candidate of LOCKFILE_PRIORITY) {
9599
- const lockPath = path51.join(dir, candidate.lockfile);
9881
+ const lockPath = path52.join(dir, candidate.lockfile);
9600
9882
  if (await exists2(lockPath)) {
9601
9883
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
9602
9884
  }
9603
9885
  }
9604
- const parent = path51.dirname(dir);
9886
+ const parent = path52.dirname(dir);
9605
9887
  if (parent === dir) break;
9606
9888
  dir = parent;
9607
9889
  }
9608
- return { pm: "npm", cwd: path51.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
9890
+ return { pm: "npm", cwd: path52.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
9609
9891
  }
9610
9892
  async function runPackageManagerInstall(cmd) {
9611
9893
  return new Promise((resolve) => {
@@ -9654,7 +9936,7 @@ async function fileExists2(p) {
9654
9936
  }
9655
9937
  }
9656
9938
  async function readPackageJson(scanPath) {
9657
- const pkgPath = path52.join(scanPath, "package.json");
9939
+ const pkgPath = path53.join(scanPath, "package.json");
9658
9940
  const raw = await fs28.readFile(pkgPath, "utf8");
9659
9941
  return JSON.parse(raw);
9660
9942
  }
@@ -9673,11 +9955,11 @@ async function findHookFiles(scanPath) {
9673
9955
  for (const entry of entries) {
9674
9956
  if (entry.isDirectory()) {
9675
9957
  if (entry.name.startsWith(".") || HOOK_WALK_SKIP_DIRS.has(entry.name)) continue;
9676
- await walk6(path52.join(dir, entry.name));
9958
+ await walk6(path53.join(dir, entry.name));
9677
9959
  } else if (entry.isFile()) {
9678
9960
  if ((entry.name.startsWith("instrumentation") || entry.name.startsWith("otel-init")) && /\.(ts|js|cjs|mjs)$/.test(entry.name)) {
9679
- const rel = path52.relative(scanPath, path52.join(dir, entry.name));
9680
- found.push(rel.split(path52.sep).join("/"));
9961
+ const rel = path53.relative(scanPath, path53.join(dir, entry.name));
9962
+ found.push(rel.split(path53.sep).join("/"));
9681
9963
  }
9682
9964
  }
9683
9965
  }
@@ -9688,7 +9970,7 @@ async function findHookFiles(scanPath) {
9688
9970
  async function pickPrimaryHookFile(scanPath, hookFiles, snippet2) {
9689
9971
  let fallback = null;
9690
9972
  for (const file of hookFiles) {
9691
- const content = await fs28.readFile(path52.join(scanPath, file), "utf8");
9973
+ const content = await fs28.readFile(path53.join(scanPath, file), "utf8");
9692
9974
  const patched = splicedContent(content, snippet2);
9693
9975
  if (patched !== null) return { file, content, patched };
9694
9976
  if (fallback === null) fallback = { file, content };
@@ -9696,11 +9978,11 @@ async function pickPrimaryHookFile(scanPath, hookFiles, snippet2) {
9696
9978
  return { file: fallback.file, content: fallback.content, patched: null };
9697
9979
  }
9698
9980
  function extendLogPath() {
9699
- return process.env.NEAT_EXTEND_LOG ?? path52.join(os3.homedir(), ".neat", "extend-log.ndjson");
9981
+ return process.env.NEAT_EXTEND_LOG ?? path53.join(os3.homedir(), ".neat", "extend-log.ndjson");
9700
9982
  }
9701
9983
  async function appendExtendLog(entry) {
9702
9984
  const logPath = extendLogPath();
9703
- await fs28.mkdir(path52.dirname(logPath), { recursive: true });
9985
+ await fs28.mkdir(path53.dirname(logPath), { recursive: true });
9704
9986
  await fs28.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
9705
9987
  }
9706
9988
  function splicedContent(fileContent, snippet2) {
@@ -9759,7 +10041,7 @@ function lookupInstrumentation(library, installedVersion) {
9759
10041
  }
9760
10042
  async function describeProjectInstrumentation(ctx) {
9761
10043
  const hookFiles = await findHookFiles(ctx.scanPath);
9762
- const envNeat = await fileExists2(path52.join(ctx.scanPath, ".env.neat"));
10044
+ const envNeat = await fileExists2(path53.join(ctx.scanPath, ".env.neat"));
9763
10045
  const registryInstrPackages = new Set(
9764
10046
  registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
9765
10047
  );
@@ -9781,7 +10063,7 @@ async function applyExtension(ctx, args, options) {
9781
10063
  );
9782
10064
  }
9783
10065
  for (const file of hookFiles) {
9784
- const content = await fs28.readFile(path52.join(ctx.scanPath, file), "utf8");
10066
+ const content = await fs28.readFile(path53.join(ctx.scanPath, file), "utf8");
9785
10067
  if (content.includes(args.registration_snippet)) {
9786
10068
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
9787
10069
  }
@@ -9793,10 +10075,10 @@ async function applyExtension(ctx, args, options) {
9793
10075
  );
9794
10076
  }
9795
10077
  const primaryFile = primary.file;
9796
- const primaryPath = path52.join(ctx.scanPath, primaryFile);
10078
+ const primaryPath = path53.join(ctx.scanPath, primaryFile);
9797
10079
  const filesTouched = [];
9798
10080
  const depsAdded = [];
9799
- const pkgPath = path52.join(ctx.scanPath, "package.json");
10081
+ const pkgPath = path53.join(ctx.scanPath, "package.json");
9800
10082
  const pkg = await readPackageJson(ctx.scanPath);
9801
10083
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
9802
10084
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
@@ -9835,7 +10117,7 @@ async function dryRunExtension(ctx, args) {
9835
10117
  };
9836
10118
  }
9837
10119
  for (const file of hookFiles) {
9838
- const content = await fs28.readFile(path52.join(ctx.scanPath, file), "utf8");
10120
+ const content = await fs28.readFile(path53.join(ctx.scanPath, file), "utf8");
9839
10121
  if (content.includes(args.registration_snippet)) {
9840
10122
  return {
9841
10123
  library: args.library,
@@ -9876,7 +10158,7 @@ async function rollbackExtension(ctx, args) {
9876
10158
  if (!match) {
9877
10159
  return { undone: false, message: "no apply found for library" };
9878
10160
  }
9879
- const pkgPath = path52.join(ctx.scanPath, "package.json");
10161
+ const pkgPath = path53.join(ctx.scanPath, "package.json");
9880
10162
  if (await fileExists2(pkgPath)) {
9881
10163
  const pkg = await readPackageJson(ctx.scanPath);
9882
10164
  if (pkg.dependencies?.[match.instrumentation_package]) {
@@ -9887,7 +10169,7 @@ async function rollbackExtension(ctx, args) {
9887
10169
  }
9888
10170
  const hookFiles = await findHookFiles(ctx.scanPath);
9889
10171
  for (const file of hookFiles) {
9890
- const filePath = path52.join(ctx.scanPath, file);
10172
+ const filePath = path53.join(ctx.scanPath, file);
9891
10173
  const content = await fs28.readFile(filePath, "utf8");
9892
10174
  if (content.includes(match.registration_snippet)) {
9893
10175
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
@@ -10002,7 +10284,7 @@ data: ${JSON.stringify(envelope.payload)}
10002
10284
 
10003
10285
  // src/connectors-config.ts
10004
10286
  import os4 from "os";
10005
- import path53 from "path";
10287
+ import path54 from "path";
10006
10288
  import { promises as fs29 } from "fs";
10007
10289
  var CONNECTORS_CONFIG_VERSION = 1;
10008
10290
  var EnvRefUnsetError = class extends Error {
@@ -10017,11 +10299,11 @@ var EnvRefUnsetError = class extends Error {
10017
10299
  };
10018
10300
  function neatHome2() {
10019
10301
  const override = process.env.NEAT_HOME;
10020
- if (override && override.length > 0) return path53.resolve(override);
10021
- return path53.join(os4.homedir(), ".neat");
10302
+ if (override && override.length > 0) return path54.resolve(override);
10303
+ return path54.join(os4.homedir(), ".neat");
10022
10304
  }
10023
10305
  function connectorsConfigPath(home = neatHome2()) {
10024
- return path53.join(home, "connectors.json");
10306
+ return path54.join(home, "connectors.json");
10025
10307
  }
10026
10308
  var MODE_MASK_LOOSER_THAN_0600 = 63;
10027
10309
  async function warnIfModeLooserThan0600(file) {
@@ -10152,7 +10434,7 @@ function connectorMatchesProject(entry, project) {
10152
10434
  var CONNECTORS_LOCK_TIMEOUT_MS = 5e3;
10153
10435
  var CONNECTORS_LOCK_RETRY_MS = 50;
10154
10436
  function connectorsConfigLockPath(home = neatHome2()) {
10155
- return path53.join(home, "connectors.json.lock");
10437
+ return path54.join(home, "connectors.json.lock");
10156
10438
  }
10157
10439
  function isEnvRef(value) {
10158
10440
  return value.length > 1 && value.startsWith("$");
@@ -10165,7 +10447,7 @@ function redactCredentialRef(ref) {
10165
10447
  return out;
10166
10448
  }
10167
10449
  async function writeConfigAtomically0600(file, contents) {
10168
- await fs29.mkdir(path53.dirname(file), { recursive: true });
10450
+ await fs29.mkdir(path54.dirname(file), { recursive: true });
10169
10451
  const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
10170
10452
  const fd = await fs29.open(tmp, "w", 384);
10171
10453
  try {
@@ -10179,7 +10461,7 @@ async function writeConfigAtomically0600(file, contents) {
10179
10461
  }
10180
10462
  async function acquireConnectorsLock(lockPath, timeoutMs = CONNECTORS_LOCK_TIMEOUT_MS) {
10181
10463
  const deadline = Date.now() + timeoutMs;
10182
- await fs29.mkdir(path53.dirname(lockPath), { recursive: true });
10464
+ await fs29.mkdir(path54.dirname(lockPath), { recursive: true });
10183
10465
  for (; ; ) {
10184
10466
  try {
10185
10467
  const fd = await fs29.open(lockPath, "wx");
@@ -10327,14 +10609,14 @@ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_
10327
10609
  }
10328
10610
 
10329
10611
  // src/connectors/index.ts
10330
- import { NodeType as NodeType19, parseFileId as parseFileId2, Provenance as Provenance22 } from "@neat.is/types";
10612
+ import { NodeType as NodeType20, parseFileId as parseFileId2, Provenance as Provenance23 } from "@neat.is/types";
10331
10613
  var NO_ENV = "unknown";
10332
10614
  function staticCallSiteFor(graph, serviceName, targetNodeId) {
10333
10615
  if (!graph.hasNode(targetNodeId)) return void 0;
10334
10616
  const sites = [];
10335
10617
  for (const edgeId of graph.inboundEdges(targetNodeId)) {
10336
10618
  const edge = graph.getEdgeAttributes(edgeId);
10337
- if (edge.provenance !== Provenance22.EXTRACTED) continue;
10619
+ if (edge.provenance !== Provenance23.EXTRACTED) continue;
10338
10620
  const parsed = parseFileId2(edge.source);
10339
10621
  if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
10340
10622
  const site = { relPath: edge.evidence.file };
@@ -10346,7 +10628,7 @@ function staticCallSiteFor(graph, serviceName, targetNodeId) {
10346
10628
  function routeCallSiteFor(graph, targetNodeId) {
10347
10629
  if (!graph.hasNode(targetNodeId)) return void 0;
10348
10630
  const attrs = graph.getNodeAttributes(targetNodeId);
10349
- if (attrs.type !== NodeType19.RouteNode || !attrs.path) return void 0;
10631
+ if (attrs.type !== NodeType20.RouteNode || !attrs.path) return void 0;
10350
10632
  const site = { relPath: attrs.path };
10351
10633
  if (attrs.line !== void 0) site.line = attrs.line;
10352
10634
  return site;
@@ -10366,6 +10648,7 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
10366
10648
  const { kind, name, provider } = resolved.ensureInfraNode;
10367
10649
  ensureInfraNode(graph, kind, name, provider);
10368
10650
  }
10651
+ mergeObservedColumns(graph, resolved.targetNodeId, signal.columns);
10369
10652
  const serviceNodeId = ensureServiceNode(graph, resolved.serviceName, NO_ENV);
10370
10653
  const callSite = signal.callSite ? { relPath: signal.callSite.file, line: signal.callSite.line } : routeCallSiteFor(graph, resolved.targetNodeId) ?? staticCallSiteFor(graph, resolved.serviceName, resolved.targetNodeId);
10371
10654
  const sourceId = callSite ? ensureObservedFileNode(graph, resolved.serviceName, serviceNodeId, callSite) : serviceNodeId;
@@ -10809,10 +11092,10 @@ var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
10809
11092
  // src/connectors/supabase/map.ts
10810
11093
  var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
10811
11094
  var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
10812
- function targetFromRestPath(path54) {
10813
- const rpcMatch = REST_RPC_PATH_RE.exec(path54);
11095
+ function targetFromRestPath(path55) {
11096
+ const rpcMatch = REST_RPC_PATH_RE.exec(path55);
10814
11097
  if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
10815
- const tableMatch = REST_TABLE_PATH_RE.exec(path54);
11098
+ const tableMatch = REST_TABLE_PATH_RE.exec(path55);
10816
11099
  if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
10817
11100
  return null;
10818
11101
  }
@@ -10876,12 +11159,14 @@ function diffPgStatStatementsToSignals(rows, previous, nowIso2) {
10876
11159
  if (delta <= 0) continue;
10877
11160
  const table = tableNameFromQueryText(row.query);
10878
11161
  if (!table) continue;
11162
+ const columns = columnsFromSqlStatement(row.query);
10879
11163
  signals.push({
10880
11164
  targetKind: SUPABASE_TABLE_TARGET_KIND,
10881
11165
  targetName: table,
10882
11166
  callCount: delta,
10883
11167
  errorCount: 0,
10884
- lastObservedIso: nowIso2
11168
+ lastObservedIso: nowIso2,
11169
+ ...columns.length > 0 ? { columns } : {}
10885
11170
  });
10886
11171
  }
10887
11172
  for (const queryid of [...previous.keys()]) {
@@ -10919,21 +11204,21 @@ async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT
10919
11204
  }
10920
11205
 
10921
11206
  // src/connectors/supabase/resolve.ts
10922
- import { EdgeType as EdgeType23, infraId as infraId12 } from "@neat.is/types";
11207
+ import { EdgeType as EdgeType23, infraId as infraId13 } from "@neat.is/types";
10923
11208
  function createSupabaseResolveTarget(graph, config) {
10924
11209
  return (signal, _ctx) => {
10925
11210
  if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
10926
11211
  return null;
10927
11212
  }
10928
- const subResourceId = infraId12(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
11213
+ const subResourceId = infraId13(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
10929
11214
  if (graph.hasNode(subResourceId)) {
10930
11215
  return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: EdgeType23.CALLS };
10931
11216
  }
10932
- const bareResourceId = infraId12(signal.targetKind, signal.targetName);
11217
+ const bareResourceId = infraId13(signal.targetKind, signal.targetName);
10933
11218
  if (graph.hasNode(bareResourceId)) {
10934
11219
  return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: EdgeType23.CALLS };
10935
11220
  }
10936
- const projectLevelId = infraId12("supabase", config.nodeRef);
11221
+ const projectLevelId = infraId13("supabase", config.nodeRef);
10937
11222
  if (graph.hasNode(projectLevelId)) {
10938
11223
  return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: EdgeType23.CALLS };
10939
11224
  }
@@ -11027,7 +11312,7 @@ function createSupabaseConnector(graph, config, deps = {}) {
11027
11312
  }
11028
11313
 
11029
11314
  // src/connectors/railway/index.ts
11030
- import { EdgeType as EdgeType24, NodeType as NodeType20, serviceId as serviceId6 } from "@neat.is/types";
11315
+ import { EdgeType as EdgeType24, NodeType as NodeType21, serviceId as serviceId6 } from "@neat.is/types";
11031
11316
 
11032
11317
  // src/connectors/railway/client.ts
11033
11318
  var DEFAULT_RAILWAY_API_URL = "https://backboard.railway.com/graphql/v2";
@@ -11177,7 +11462,7 @@ function buildRailwayRouteIndex(graph, serviceName) {
11177
11462
  const out = [];
11178
11463
  graph.forEachNode((_id, attrs) => {
11179
11464
  const node = attrs;
11180
- if (node.type !== NodeType20.RouteNode) return;
11465
+ if (node.type !== NodeType21.RouteNode) return;
11181
11466
  const route = attrs;
11182
11467
  if (route.service !== serviceName) return;
11183
11468
  out.push({
@@ -11405,9 +11690,9 @@ function parseFirebaseTargetName(targetName) {
11405
11690
  const secondSep = rest.indexOf(FIELD_SEP);
11406
11691
  if (secondSep === -1) return null;
11407
11692
  const method = rest.slice(0, secondSep);
11408
- const path54 = rest.slice(secondSep + 1);
11409
- if (!resourceName || !method || !path54) return null;
11410
- return { resourceName, method, path: path54 };
11693
+ const path55 = rest.slice(secondSep + 1);
11694
+ if (!resourceName || !method || !path55) return null;
11695
+ return { resourceName, method, path: path55 };
11411
11696
  }
11412
11697
  function resourceNameFor(type, labels) {
11413
11698
  if (!labels) return null;
@@ -11445,14 +11730,14 @@ function mapLogEntryToSignal(entry) {
11445
11730
  if (!req) return null;
11446
11731
  if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
11447
11732
  const method = req.requestMethod.toUpperCase();
11448
- const path54 = pathFromRequestUrl(req.requestUrl);
11449
- if (path54 === null) return null;
11733
+ const path55 = pathFromRequestUrl(req.requestUrl);
11734
+ if (path55 === null) return null;
11450
11735
  const timestamp = entry.timestamp;
11451
11736
  if (typeof timestamp !== "string" || timestamp.length === 0) return null;
11452
11737
  const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD2;
11453
11738
  return {
11454
11739
  targetKind: resourceType,
11455
- targetName: packFirebaseTargetName({ resourceName, method, path: path54 }),
11740
+ targetName: packFirebaseTargetName({ resourceName, method, path: path55 }),
11456
11741
  callCount: 1,
11457
11742
  errorCount: isError ? 1 : 0,
11458
11743
  lastObservedIso: timestamp
@@ -11468,7 +11753,7 @@ function mapLogEntriesToSignals(entries) {
11468
11753
  }
11469
11754
 
11470
11755
  // src/connectors/firebase/resolve.ts
11471
- import { NodeType as NodeType21, EdgeType as EdgeType25 } from "@neat.is/types";
11756
+ import { NodeType as NodeType22, EdgeType as EdgeType25 } from "@neat.is/types";
11472
11757
  function neatServiceNameFor(resourceType, resourceName, serviceMap) {
11473
11758
  switch (resourceType) {
11474
11759
  case "cloud_function":
@@ -11483,7 +11768,7 @@ function routeEntriesFor(graph, serviceName) {
11483
11768
  const entries = [];
11484
11769
  graph.forEachNode((_id, attrs) => {
11485
11770
  const node = attrs;
11486
- if (node.type !== NodeType21.RouteNode) return;
11771
+ if (node.type !== NodeType22.RouteNode) return;
11487
11772
  const route = attrs;
11488
11773
  if (route.service !== serviceName) return;
11489
11774
  entries.push({
@@ -11538,7 +11823,7 @@ function createFirebaseConnector(graph, serviceMap) {
11538
11823
  }
11539
11824
 
11540
11825
  // src/connectors/cloudflare/connector.ts
11541
- import { EdgeType as EdgeType26, NodeType as NodeType22, fileId as fileId4, infraId as infraId13 } from "@neat.is/types";
11826
+ import { EdgeType as EdgeType26, NodeType as NodeType23, fileId as fileId4, infraId as infraId14 } from "@neat.is/types";
11542
11827
 
11543
11828
  // src/connectors/cloudflare/client.ts
11544
11829
  import { randomUUID } from "crypto";
@@ -11649,7 +11934,7 @@ function mapEventToSignal(event) {
11649
11934
  if (Number.isNaN(observedAt.getTime())) return null;
11650
11935
  const statusCode = metadata?.statusCode;
11651
11936
  const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
11652
- const path54 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
11937
+ const path55 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
11653
11938
  return {
11654
11939
  targetKind: CLOUDFLARE_TARGET_KIND,
11655
11940
  targetName: scriptName,
@@ -11657,7 +11942,7 @@ function mapEventToSignal(event) {
11657
11942
  errorCount: isError ? 1 : 0,
11658
11943
  lastObservedIso: observedAt.toISOString(),
11659
11944
  method,
11660
- ...path54 ? { path: path54 } : {},
11945
+ ...path55 ? { path: path55 } : {},
11661
11946
  ...typeof statusCode === "number" ? { statusCode } : {},
11662
11947
  ...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
11663
11948
  };
@@ -11697,19 +11982,19 @@ function findTaggedWorkerFileNode(graph, workerName) {
11697
11982
  graph.forEachNode((id, attrs) => {
11698
11983
  if (found) return;
11699
11984
  const a = attrs;
11700
- if (a.type === NodeType22.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
11985
+ if (a.type === NodeType23.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
11701
11986
  found = id;
11702
11987
  }
11703
11988
  });
11704
11989
  return found;
11705
11990
  }
11706
- function findMatchingRouteNode(graph, serviceName, method, path54) {
11707
- const normalizedPath = normalizePathTemplate(path54);
11991
+ function findMatchingRouteNode(graph, serviceName, method, path55) {
11992
+ const normalizedPath = normalizePathTemplate(path55);
11708
11993
  let found = null;
11709
11994
  graph.forEachNode((id, attrs) => {
11710
11995
  if (found) return;
11711
11996
  const a = attrs;
11712
- if (a.type !== NodeType22.RouteNode || a.service !== serviceName) return;
11997
+ if (a.type !== NodeType23.RouteNode || a.service !== serviceName) return;
11713
11998
  if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
11714
11999
  const routeMethod = (a.method ?? "").toUpperCase();
11715
12000
  if (routeMethod !== "ALL" && routeMethod !== method) return;
@@ -11721,10 +12006,10 @@ function createCloudflareResolveTarget(config, graph) {
11721
12006
  return (signal) => {
11722
12007
  if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
11723
12008
  const scriptName = signal.targetName;
11724
- const { method, path: path54 } = signal;
12009
+ const { method, path: path55 } = signal;
11725
12010
  const resolveRouteGrain = (serviceName, wholeFileId) => {
11726
- if (!method || !path54) return wholeFileId;
11727
- return findMatchingRouteNode(graph, serviceName, method, path54) ?? wholeFileId;
12011
+ if (!method || !path55) return wholeFileId;
12012
+ return findMatchingRouteNode(graph, serviceName, method, path55) ?? wholeFileId;
11728
12013
  };
11729
12014
  const mapping = config.workers?.[scriptName];
11730
12015
  if (mapping) {
@@ -11745,7 +12030,7 @@ function createCloudflareResolveTarget(config, graph) {
11745
12030
  };
11746
12031
  }
11747
12032
  return {
11748
- targetNodeId: infraId13("cloudflare-worker", scriptName),
12033
+ targetNodeId: infraId14("cloudflare-worker", scriptName),
11749
12034
  serviceName: scriptName,
11750
12035
  edgeType: EdgeType26.CALLS,
11751
12036
  ensureInfraNode: { kind: "cloudflare-worker", name: scriptName, provider: "cloudflare" }
@@ -11912,12 +12197,14 @@ function diffNeonStatementsToSignals(rows, previous, observedAtIso) {
11912
12197
  if (!prior || calls <= prior.calls) continue;
11913
12198
  const table = tableFromSqlStatement(row.query);
11914
12199
  if (!table) continue;
12200
+ const columns = columnsFromSqlStatement(row.query);
11915
12201
  signals.push({
11916
12202
  targetKind: NEON_SQL_TABLE_TARGET_KIND,
11917
12203
  targetName: table,
11918
12204
  callCount: calls - prior.calls,
11919
12205
  errorCount: 0,
11920
- lastObservedIso: observedAtIso
12206
+ lastObservedIso: observedAtIso,
12207
+ ...columns.length > 0 ? { columns } : {}
11921
12208
  });
11922
12209
  }
11923
12210
  for (const queryid of previous.keys()) {
@@ -11927,12 +12214,12 @@ function diffNeonStatementsToSignals(rows, previous, observedAtIso) {
11927
12214
  }
11928
12215
 
11929
12216
  // src/connectors/neon/resolve.ts
11930
- import { EdgeType as EdgeType27, infraId as infraId14 } from "@neat.is/types";
12217
+ import { EdgeType as EdgeType27, infraId as infraId15 } from "@neat.is/types";
11931
12218
  function createNeonResolveTarget(config) {
11932
12219
  return (signal) => {
11933
12220
  if (signal.targetKind !== NEON_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
11934
12221
  return {
11935
- targetNodeId: infraId14("sql-table", signal.targetName),
12222
+ targetNodeId: infraId15("sql-table", signal.targetName),
11936
12223
  serviceName: config.serviceName,
11937
12224
  edgeType: EdgeType27.CALLS,
11938
12225
  ensureInfraNode: { kind: "sql-table", name: signal.targetName, provider: "neon" }
@@ -13327,4 +13614,4 @@ export {
13327
13614
  deprovisionConnector,
13328
13615
  buildApi
13329
13616
  };
13330
- //# sourceMappingURL=chunk-5RIL3U5A.js.map
13617
+ //# sourceMappingURL=chunk-RR4LWQQB.js.map