@neat.is/core 0.6.0 → 0.6.2-dev.20260722

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,7 +1,7 @@
1
1
  import {
2
2
  mountBearerAuth,
3
3
  readAuthEnv
4
- } from "./chunk-BZ3AJVAC.js";
4
+ } from "./chunk-2MAGU6RB.js";
5
5
 
6
6
  // src/graph.ts
7
7
  import GraphDefault from "graphology";
@@ -339,969 +339,454 @@ function deprecatedApis() {
339
339
  return currentMatrix().deprecatedApis ?? [];
340
340
  }
341
341
 
342
- // src/traverse.ts
342
+ // src/ingest.ts
343
+ import { promises as fs3, existsSync, readFileSync } from "fs";
344
+ import path3 from "path";
345
+ import * as sourceMapJs from "source-map-js";
346
+
347
+ // src/policy.ts
348
+ import { promises as fs2 } from "fs";
349
+ import path2 from "path";
343
350
  import {
344
- BlastRadiusResultSchema,
345
351
  EdgeType,
346
352
  NodeType,
347
- ObservedDependenciesResultSchema,
348
- PROV_RANK,
349
- Provenance,
350
- RootCauseResultSchema,
351
- TransitiveDependenciesResultSchema
353
+ PolicyFileSchema
352
354
  } from "@neat.is/types";
353
- var ROOT_CAUSE_MAX_DEPTH = 5;
354
- var BLAST_RADIUS_DEFAULT_DEPTH = 10;
355
- function isFrontierNode(graph, nodeId) {
356
- if (!graph.hasNode(nodeId)) return false;
357
- const attrs = graph.getNodeAttributes(nodeId);
358
- return attrs.type === NodeType.FrontierNode;
359
- }
360
- function resolveOwningService(graph, nodeId) {
361
- if (!graph.hasNode(nodeId)) return null;
362
- const attrs = graph.getNodeAttributes(nodeId);
363
- if (attrs.type === NodeType.ServiceNode) {
364
- return { id: nodeId, svc: attrs };
365
- }
366
- if (attrs.type === NodeType.FileNode) {
367
- for (const edgeId of graph.inboundEdges(nodeId)) {
368
- const e = graph.getEdgeAttributes(edgeId);
369
- if (e.type !== EdgeType.CONTAINS) continue;
370
- const owner = graph.getNodeAttributes(e.source);
371
- if (owner.type === NodeType.ServiceNode) {
372
- return { id: e.source, svc: owner };
373
- }
374
- }
375
- }
376
- return null;
377
- }
378
- function bestEdgeBySource(graph, edgeIds) {
379
- const best = /* @__PURE__ */ new Map();
380
- for (const id of edgeIds) {
381
- const e = graph.getEdgeAttributes(id);
382
- if (isFrontierNode(graph, e.source)) continue;
383
- const cur = best.get(e.source);
384
- if (!cur || PROV_RANK[e.provenance] > PROV_RANK[cur.provenance]) {
385
- best.set(e.source, e);
386
- }
387
- }
388
- return best;
389
- }
390
- function bestEdgeByTarget(graph, edgeIds) {
391
- const best = /* @__PURE__ */ new Map();
392
- for (const id of edgeIds) {
393
- const e = graph.getEdgeAttributes(id);
394
- if (isFrontierNode(graph, e.target)) continue;
395
- const cur = best.get(e.target);
396
- if (!cur || PROV_RANK[e.provenance] > PROV_RANK[cur.provenance]) {
397
- best.set(e.target, e);
398
- }
399
- }
400
- return best;
401
- }
402
- var PROVENANCE_CEILING = {
403
- OBSERVED: 1,
404
- INFERRED: 0.7,
405
- EXTRACTED: 0.5,
406
- STALE: 0.3
355
+
356
+ // src/events.ts
357
+ import { EventEmitter } from "events";
358
+ var EVENT_BUS_CHANNEL = "event";
359
+ var NeatEventBus = class extends EventEmitter {
407
360
  };
408
- function volumeWeight(spanCount) {
409
- if (!spanCount || spanCount <= 0) return 0.5;
410
- const w = 0.5 + Math.log10(spanCount + 1) / 3;
411
- return Math.min(1, w);
412
- }
413
- function recencyWeight(ageMs) {
414
- if (ageMs === void 0) return 0.8;
415
- const hour = 60 * 60 * 1e3;
416
- if (ageMs <= hour) return 1;
417
- if (ageMs <= 24 * hour) {
418
- const t = (ageMs - hour) / (23 * hour);
419
- return 1 - 0.5 * t;
420
- }
421
- return 0.3;
422
- }
423
- function cleanlinessWeight(spanCount, errorCount) {
424
- if (!spanCount || spanCount <= 0) return 1;
425
- const rate = (errorCount ?? 0) / spanCount;
426
- if (rate <= 0.01) return 1;
427
- if (rate >= 0.5) return 0.3;
428
- return 1 - rate * 1.4;
429
- }
430
- function confidenceForEdge(edge, now = Date.now()) {
431
- const ceiling = PROVENANCE_CEILING[edge.provenance] ?? 0.5;
432
- const spanCount = edge.signal?.spanCount ?? edge.callCount;
433
- const ageMs = edge.signal?.lastObservedAgeMs ?? lastObservedAge(edge, now);
434
- if (spanCount === void 0 && ageMs === void 0 && edge.signal === void 0) {
435
- return ceiling;
436
- }
437
- const v = volumeWeight(spanCount);
438
- const r = recencyWeight(ageMs);
439
- const c = cleanlinessWeight(spanCount, edge.signal?.errorCount);
440
- return Math.max(0, Math.min(1, ceiling * v * r * c));
361
+ var eventBus = new NeatEventBus();
362
+ eventBus.setMaxListeners(0);
363
+ function emitNeatEvent(envelope) {
364
+ eventBus.emit(EVENT_BUS_CHANNEL, envelope);
441
365
  }
442
- function lastObservedAge(edge, now) {
443
- if (!edge.lastObserved) return void 0;
444
- const t = Date.parse(edge.lastObserved);
445
- if (!Number.isFinite(t)) return void 0;
446
- return Math.max(0, now - t);
366
+ function attachGraphToEventBus(graph, opts) {
367
+ const { project } = opts;
368
+ const onNodeAdded = (payload) => {
369
+ emitNeatEvent({
370
+ type: "node-added",
371
+ project,
372
+ payload: { node: payload.attributes }
373
+ });
374
+ };
375
+ const onNodeDropped = (payload) => {
376
+ emitNeatEvent({
377
+ type: "node-removed",
378
+ project,
379
+ payload: { id: payload.key }
380
+ });
381
+ };
382
+ const onEdgeAdded = (payload) => {
383
+ emitNeatEvent({
384
+ type: "edge-added",
385
+ project,
386
+ payload: { edge: payload.attributes }
387
+ });
388
+ };
389
+ const onEdgeDropped = (payload) => {
390
+ emitNeatEvent({
391
+ type: "edge-removed",
392
+ project,
393
+ payload: { id: payload.key }
394
+ });
395
+ };
396
+ const onNodeAttrsUpdated = (payload) => {
397
+ emitNeatEvent({
398
+ type: "node-updated",
399
+ project,
400
+ payload: { id: payload.key, changes: payload.attributes }
401
+ });
402
+ };
403
+ graph.on("nodeAdded", onNodeAdded);
404
+ graph.on("nodeDropped", onNodeDropped);
405
+ graph.on("edgeAdded", onEdgeAdded);
406
+ graph.on("edgeDropped", onEdgeDropped);
407
+ graph.on("nodeAttributesUpdated", onNodeAttrsUpdated);
408
+ return () => {
409
+ graph.off("nodeAdded", onNodeAdded);
410
+ graph.off("nodeDropped", onNodeDropped);
411
+ graph.off("edgeAdded", onEdgeAdded);
412
+ graph.off("edgeDropped", onEdgeDropped);
413
+ graph.off("nodeAttributesUpdated", onNodeAttrsUpdated);
414
+ };
447
415
  }
448
- function confidenceFromMix(edges, now = Date.now()) {
449
- if (edges.length === 0) return 1;
450
- let product = 1;
451
- for (const e of edges) {
452
- product *= confidenceForEdge(e, now);
453
- }
454
- return Math.max(0, Math.min(1, product));
416
+
417
+ // src/policy.ts
418
+ var DEFAULT_ACTION_BY_SEVERITY = {
419
+ info: "log",
420
+ warning: "alert",
421
+ error: "alert",
422
+ critical: "block"
423
+ };
424
+ function resolveOnViolation(policy) {
425
+ return policy.onViolation ?? DEFAULT_ACTION_BY_SEVERITY[policy.severity];
455
426
  }
456
- function longestIncomingWalk(graph, start, maxDepth) {
457
- let best = { path: [start], edges: [] };
458
- const visited = /* @__PURE__ */ new Set([start]);
459
- function step(node, path48, edges) {
460
- if (path48.length > best.path.length) {
461
- best = { path: [...path48], edges: [...edges] };
462
- }
463
- if (path48.length - 1 >= maxDepth) return;
464
- const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
465
- for (const [srcId, edge] of incoming) {
466
- if (visited.has(srcId)) continue;
467
- visited.add(srcId);
468
- path48.push(srcId);
469
- edges.push(edge);
470
- step(srcId, path48, edges);
471
- path48.pop();
472
- edges.pop();
473
- visited.delete(srcId);
474
- }
475
- }
476
- step(start, [start], []);
477
- return best;
427
+ function makeViolation(policy, rule, contextSuffix, message, subject, ctx) {
428
+ return {
429
+ id: `${policy.id}:${contextSuffix}`,
430
+ policyId: policy.id,
431
+ policyName: policy.name,
432
+ severity: policy.severity,
433
+ onViolation: resolveOnViolation(policy),
434
+ ruleType: rule.type,
435
+ subject,
436
+ message,
437
+ observedAt: new Date(ctx.now()).toISOString()
438
+ };
478
439
  }
479
- function databaseRootCauseShape(graph, origin, walk3) {
480
- const targetDb = origin;
481
- const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
482
- if (candidatePairs.length === 0) return null;
483
- for (const id of walk3.path) {
484
- const owner = resolveOwningService(graph, id);
485
- if (!owner) continue;
486
- const { id: serviceId5, svc } = owner;
487
- const deps = svc.dependencies ?? {};
488
- for (const pair of candidatePairs) {
489
- const declared = deps[pair.driver];
490
- if (!declared) continue;
491
- const result = checkCompatibility(
492
- pair.driver,
493
- declared,
494
- targetDb.engine,
495
- targetDb.engineVersion
496
- );
497
- if (!result.compatible) {
498
- return {
499
- rootCauseNode: serviceId5,
500
- rootCauseReason: result.reason ?? "incompatible driver",
501
- ...result.minDriverVersion ? {
502
- fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
503
- } : {}
504
- };
440
+ var evaluateStructural = ({
441
+ graph,
442
+ policy,
443
+ rule,
444
+ ctx
445
+ }) => {
446
+ const violations = [];
447
+ graph.forEachNode((id, attrs) => {
448
+ const a = attrs;
449
+ if (a.type !== rule.fromNodeType) return;
450
+ let satisfied = false;
451
+ for (const edgeId of graph.outboundEdges(id)) {
452
+ const e = graph.getEdgeAttributes(edgeId);
453
+ if (e.type !== rule.edgeType) continue;
454
+ const target = graph.getNodeAttributes(e.target);
455
+ if (target.type === NodeType.FrontierNode) continue;
456
+ if (target.type === rule.toNodeType) {
457
+ satisfied = true;
458
+ break;
505
459
  }
506
460
  }
507
- }
508
- return null;
509
- }
510
- function serviceRootCauseShape(graph, _origin, walk3) {
511
- for (const id of walk3.path) {
512
- const owner = resolveOwningService(graph, id);
513
- if (!owner) continue;
514
- const { id: serviceId5, svc } = owner;
515
- const deps = svc.dependencies ?? {};
516
- const serviceNodeEngine = svc.nodeEngine;
517
- for (const constraint of nodeEngineConstraints()) {
518
- const declared = deps[constraint.package];
519
- if (!declared) continue;
520
- const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
521
- if (!result.compatible && result.reason) {
522
- return {
523
- rootCauseNode: serviceId5,
524
- rootCauseReason: result.reason,
525
- ...result.requiredNodeVersion ? {
526
- fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
527
- } : {}
528
- };
529
- }
530
- }
531
- for (const conflict of packageConflicts()) {
532
- const declared = deps[conflict.package];
533
- if (!declared) continue;
534
- const requiredDeclared = deps[conflict.requires.name];
535
- const result = checkPackageConflict(conflict, declared, requiredDeclared);
536
- if (!result.compatible && result.reason) {
537
- return {
538
- rootCauseNode: serviceId5,
539
- rootCauseReason: result.reason,
540
- fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
541
- };
542
- }
461
+ if (!satisfied) {
462
+ violations.push(
463
+ makeViolation(
464
+ policy,
465
+ rule,
466
+ id,
467
+ `${rule.fromNodeType} ${id} has no ${rule.edgeType} edge to a ${rule.toNodeType}`,
468
+ { nodeId: id },
469
+ ctx
470
+ )
471
+ );
543
472
  }
544
- }
545
- return null;
546
- }
547
- function fileRootCauseShape(graph, origin, walk3) {
548
- const owner = resolveOwningService(graph, origin.id);
549
- if (!owner) return null;
550
- return serviceRootCauseShape(graph, owner.svc, walk3);
551
- }
552
- var rootCauseShapes = {
553
- [NodeType.DatabaseNode]: databaseRootCauseShape,
554
- [NodeType.ServiceNode]: serviceRootCauseShape,
555
- [NodeType.FileNode]: fileRootCauseShape
473
+ });
474
+ return violations;
556
475
  };
557
- function getRootCause(graph, errorNodeId, errorEvent, incidents) {
558
- if (!graph.hasNode(errorNodeId)) return null;
559
- const origin = graph.getNodeAttributes(errorNodeId);
560
- const shape = rootCauseShapes[origin.type];
561
- if (shape) {
562
- const walk3 = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
563
- const match = shape(graph, origin, walk3);
564
- if (match) {
565
- const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
566
- return RootCauseResultSchema.parse({
567
- rootCauseNode: match.rootCauseNode,
568
- rootCauseReason: reason,
569
- traversalPath: walk3.path,
570
- edgeProvenances: walk3.edges.map((e) => e.provenance),
571
- confidence: confidenceFromMix(walk3.edges),
572
- fixRecommendation: match.fixRecommendation
573
- });
476
+ var evaluateOwnership = ({
477
+ graph,
478
+ policy,
479
+ rule,
480
+ ctx
481
+ }) => {
482
+ const violations = [];
483
+ graph.forEachNode((id, attrs) => {
484
+ const a = attrs;
485
+ if (a.type !== rule.nodeType) return;
486
+ const value = a[rule.field];
487
+ if (typeof value !== "string" || value.length === 0) {
488
+ violations.push(
489
+ makeViolation(
490
+ policy,
491
+ rule,
492
+ id,
493
+ `${rule.nodeType} ${id} is missing required field "${rule.field}"`,
494
+ { nodeId: id },
495
+ ctx
496
+ )
497
+ );
574
498
  }
575
- }
576
- if (origin.type === NodeType.ServiceNode) {
577
- const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
578
- if (crossService) return crossService;
579
- }
580
- return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
581
- }
582
- var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
583
- function incidentMatchesNode(ev, nodeId) {
584
- return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
585
- }
586
- function localizeFromIncidents(nodeId, incidents, errorEvent) {
587
- const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
588
- const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
589
- if (relevant.length === 0) return null;
590
- const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
591
- const attrs = latest.attributes ?? {};
592
- const filepath = typeof attrs["code.filepath"] === "string" ? attrs["code.filepath"] : void 0;
593
- const lineno = typeof attrs["code.lineno"] === "number" ? attrs["code.lineno"] : void 0;
594
- const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
595
- const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
596
- const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
597
- const count = sameMode.length;
598
- const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
599
- const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
600
- if (location) reasonParts.push(`surfaced at ${location}`);
601
- const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
602
- const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
603
- const fileNode = localizesToFile ? latest.affectedNode : void 0;
604
- const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
605
- return {
606
- rootCauseNode: fileNode ?? nodeId,
607
- rootCauseReason,
608
- ...fileNode ? { fileNode } : {},
609
- ...fixRecommendation ? { fixRecommendation } : {}
610
- };
611
- }
612
- function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
613
- const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
614
- if (!loc) return null;
615
- const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
616
- const edgeProvenances = loc.fileNode ? [Provenance.OBSERVED] : [];
617
- return RootCauseResultSchema.parse({
618
- rootCauseNode: loc.rootCauseNode,
619
- rootCauseReason: loc.rootCauseReason,
620
- traversalPath,
621
- edgeProvenances,
622
- confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
623
- ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
624
499
  });
625
- }
626
- function isFailingCallEdge(e) {
627
- return e.type === EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
628
- }
629
- function callSourcesForService(graph, serviceId5) {
630
- const ids = [serviceId5];
631
- for (const edgeId of graph.outboundEdges(serviceId5)) {
632
- const e = graph.getEdgeAttributes(edgeId);
633
- if (e.type !== EdgeType.CONTAINS) continue;
634
- const tgt = graph.getNodeAttributes(e.target);
635
- if (tgt.type === NodeType.FileNode) ids.push(e.target);
636
- }
637
- return ids;
638
- }
639
- function failingCallDominates(e, id, curEdge, curId) {
640
- const ec = e.signal?.errorCount ?? 0;
641
- const cc = curEdge.signal?.errorCount ?? 0;
642
- if (ec !== cc) return ec > cc;
643
- if (PROV_RANK[e.provenance] !== PROV_RANK[curEdge.provenance]) {
644
- return PROV_RANK[e.provenance] > PROV_RANK[curEdge.provenance];
645
- }
646
- return id < curId;
647
- }
648
- function dominantFailingCall(graph, serviceId5, visited) {
649
- let best = null;
650
- for (const src of callSourcesForService(graph, serviceId5)) {
651
- for (const edgeId of graph.outboundEdges(src)) {
652
- const e = graph.getEdgeAttributes(edgeId);
653
- if (!isFailingCallEdge(e)) continue;
654
- if (isFrontierNode(graph, e.target)) continue;
655
- const owner = resolveOwningService(graph, e.target);
656
- if (!owner || visited.has(owner.id)) continue;
657
- if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
658
- best = { nextService: owner.id, edge: e };
659
- }
500
+ return violations;
501
+ };
502
+ var evaluateProvenance = ({
503
+ graph,
504
+ policy,
505
+ rule,
506
+ ctx
507
+ }) => {
508
+ const required = Array.isArray(rule.required) ? new Set(rule.required) : /* @__PURE__ */ new Set([rule.required]);
509
+ const violations = [];
510
+ graph.forEachEdge((edgeId, attrs) => {
511
+ const e = attrs;
512
+ if (e.type !== rule.edgeType) return;
513
+ if (rule.targetNodeId && e.target !== rule.targetNodeId) return;
514
+ if (!required.has(e.provenance)) {
515
+ const requiredList = [...required].join(" | ");
516
+ violations.push(
517
+ makeViolation(
518
+ policy,
519
+ rule,
520
+ edgeId,
521
+ `${rule.edgeType} edge ${edgeId} has provenance ${e.provenance}; required ${requiredList}`,
522
+ { edgeId },
523
+ ctx
524
+ )
525
+ );
660
526
  }
661
- }
662
- return best;
663
- }
664
- function followFailingCallChain(graph, originServiceId, maxDepth) {
665
- const path48 = [originServiceId];
666
- const edges = [];
667
- const visited = /* @__PURE__ */ new Set([originServiceId]);
668
- let current = originServiceId;
669
- for (let depth = 0; depth < maxDepth; depth++) {
670
- const hop = dominantFailingCall(graph, current, visited);
671
- if (!hop) break;
672
- path48.push(hop.nextService);
673
- edges.push(hop.edge);
674
- visited.add(hop.nextService);
675
- current = hop.nextService;
676
- }
677
- if (edges.length === 0) return null;
678
- return { path: path48, edges, culprit: current };
679
- }
680
- function crossServiceRootCause(graph, originId, incidents, errorEvent) {
681
- const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
682
- if (!chain) return null;
683
- const culprit = chain.culprit;
684
- const path48 = [...chain.path];
685
- const edgeProvenances = chain.edges.map((e) => e.provenance);
686
- const baseConfidence = confidenceFromMix(chain.edges);
687
- const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
688
- const loc = localizeFromIncidents(culprit, incidents, errorEvent);
689
- if (loc) {
690
- let rootCauseNode = culprit;
691
- if (loc.fileNode) {
692
- path48.push(loc.fileNode);
693
- edgeProvenances.push(Provenance.OBSERVED);
694
- rootCauseNode = loc.fileNode;
527
+ });
528
+ return violations;
529
+ };
530
+ var evaluateBlastRadius = ({
531
+ graph,
532
+ policy,
533
+ rule,
534
+ ctx
535
+ }) => {
536
+ const violations = [];
537
+ const depth = rule.depth;
538
+ graph.forEachNode((id, attrs) => {
539
+ const a = attrs;
540
+ if (a.type !== rule.nodeType) return;
541
+ const result = depth !== void 0 ? getBlastRadius(graph, id, depth) : getBlastRadius(graph, id);
542
+ if (result.totalAffected > rule.maxAffected) {
543
+ violations.push(
544
+ makeViolation(
545
+ policy,
546
+ rule,
547
+ id,
548
+ `${rule.nodeType} ${id} has blast radius ${result.totalAffected} > ${rule.maxAffected}`,
549
+ { nodeId: id, path: [id] },
550
+ ctx
551
+ )
552
+ );
695
553
  }
696
- return RootCauseResultSchema.parse({
697
- rootCauseNode,
698
- rootCauseReason: loc.rootCauseReason,
699
- traversalPath: path48,
700
- edgeProvenances,
701
- confidence,
702
- ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
703
- });
704
- }
705
- const lastEdge = chain.edges[chain.edges.length - 1];
706
- const errs = lastEdge.signal?.errorCount ?? 0;
707
- const culpritName = culprit.replace(/^service:/, "");
708
- return RootCauseResultSchema.parse({
709
- rootCauseNode: culprit,
710
- rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
711
- traversalPath: path48,
712
- edgeProvenances,
713
- confidence,
714
- fixRecommendation: `Inspect ${culpritName}'s failing handler`
715
554
  });
716
- }
717
- function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
718
- if (!graph.hasNode(nodeId)) {
719
- return BlastRadiusResultSchema.parse({ origin: nodeId, affectedNodes: [], totalAffected: 0 });
720
- }
721
- const seen = /* @__PURE__ */ new Map();
722
- const queue = [{ nodeId, distance: 0, path: [nodeId], pathEdges: [] }];
723
- const enqueued = /* @__PURE__ */ new Set([nodeId]);
724
- while (queue.length > 0) {
725
- const frame = queue.shift();
726
- if (frame.distance > 0 && frame.pathEdges.length > 0) {
727
- const lastEdge = frame.pathEdges[frame.pathEdges.length - 1];
728
- seen.set(frame.nodeId, {
729
- nodeId: frame.nodeId,
730
- distance: frame.distance,
731
- edgeProvenance: lastEdge.provenance,
732
- path: frame.path,
733
- confidence: confidenceFromMix(frame.pathEdges)
734
- });
735
- }
736
- if (frame.distance >= maxDepth) continue;
737
- const incoming = bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId));
738
- for (const [srcId, edge] of incoming) {
739
- if (enqueued.has(srcId)) continue;
740
- enqueued.add(srcId);
741
- queue.push({
742
- nodeId: srcId,
743
- distance: frame.distance + 1,
744
- path: [...frame.path, srcId],
745
- pathEdges: [...frame.pathEdges, edge]
746
- });
747
- }
748
- }
749
- const affectedNodes = [...seen.values()].sort(
750
- (a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
751
- );
752
- return BlastRadiusResultSchema.parse({
753
- origin: nodeId,
754
- affectedNodes,
755
- totalAffected: affectedNodes.length
756
- });
757
- }
758
- var TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH = 3;
759
- var TRANSITIVE_DEPENDENCIES_MAX_DEPTH = 10;
760
- function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH) {
761
- if (!graph.hasNode(nodeId)) {
762
- return TransitiveDependenciesResultSchema.parse({
763
- origin: nodeId,
764
- depth,
765
- dependencies: [],
766
- total: 0
767
- });
768
- }
769
- const seen = /* @__PURE__ */ new Map();
770
- const queue = [{ nodeId, distance: 0, edge: null }];
771
- const enqueued = /* @__PURE__ */ new Set([nodeId]);
772
- while (queue.length > 0) {
773
- const frame = queue.shift();
774
- if (frame.distance > 0 && frame.edge && frame.edge.type !== EdgeType.CONTAINS) {
775
- seen.set(frame.nodeId, {
776
- nodeId: frame.nodeId,
777
- distance: frame.distance,
778
- edgeType: frame.edge.type,
779
- provenance: frame.edge.provenance
780
- });
781
- }
782
- if (frame.distance >= depth) continue;
783
- const outgoing = bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
784
- for (const [tgtId, edge] of outgoing) {
785
- if (enqueued.has(tgtId)) continue;
786
- enqueued.add(tgtId);
787
- queue.push({ nodeId: tgtId, distance: frame.distance + 1, edge });
555
+ return violations;
556
+ };
557
+ var evaluateCompatibility = ({
558
+ graph,
559
+ policy,
560
+ rule,
561
+ ctx
562
+ }) => {
563
+ const violations = [];
564
+ const wantsKind = (kind) => rule.kind === void 0 || rule.kind === kind;
565
+ graph.forEachNode((svcId, attrs) => {
566
+ const a = attrs;
567
+ if (a.type !== NodeType.ServiceNode) return;
568
+ const svc = a;
569
+ const deps = svc.dependencies ?? {};
570
+ if (wantsKind("driver-engine")) {
571
+ for (const edgeId of graph.outboundEdges(svcId)) {
572
+ const e = graph.getEdgeAttributes(edgeId);
573
+ if (e.type !== EdgeType.CONNECTS_TO) continue;
574
+ const dbAttrs = graph.getNodeAttributes(e.target);
575
+ if (dbAttrs.type === NodeType.FrontierNode) continue;
576
+ if (dbAttrs.type !== NodeType.DatabaseNode) continue;
577
+ const db = dbAttrs;
578
+ for (const pair of compatPairs()) {
579
+ if (pair.engine !== db.engine) continue;
580
+ const declared = deps[pair.driver];
581
+ if (!declared) continue;
582
+ const result = checkCompatibility(pair.driver, declared, db.engine, db.engineVersion);
583
+ if (!result.compatible && result.reason) {
584
+ violations.push(
585
+ makeViolation(
586
+ policy,
587
+ rule,
588
+ `${svcId}:driver-engine:${pair.driver}@${declared}:${db.engine}@${db.engineVersion}`,
589
+ result.reason,
590
+ { nodeId: svcId, edgeId },
591
+ ctx
592
+ )
593
+ );
594
+ }
595
+ }
596
+ }
788
597
  }
789
- }
790
- const dependencies = [...seen.values()].sort(
791
- (a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
792
- );
793
- return TransitiveDependenciesResultSchema.parse({
794
- origin: nodeId,
795
- depth,
796
- dependencies,
797
- total: dependencies.length
798
- });
799
- }
800
- function getObservedDependencies(graph, nodeId) {
801
- if (!graph.hasNode(nodeId)) {
802
- return ObservedDependenciesResultSchema.parse({
803
- origin: nodeId,
804
- dependencies: [],
805
- observed: false,
806
- inboundObservedCount: 0,
807
- hasExtractedOutbound: false
808
- });
809
- }
810
- const attrs = graph.getNodeAttributes(nodeId);
811
- const scope = [nodeId];
812
- if (attrs.type === NodeType.ServiceNode) {
813
- for (const edgeId of graph.outboundEdges(nodeId)) {
814
- const e = graph.getEdgeAttributes(edgeId);
815
- if (e.type !== EdgeType.CONTAINS) continue;
816
- const owned = graph.getNodeAttributes(e.target);
817
- if (owned.type === NodeType.FileNode) scope.push(e.target);
598
+ if (wantsKind("node-engine")) {
599
+ const serviceNodeRange = svc.nodeEngine;
600
+ for (const constraint of nodeEngineConstraints()) {
601
+ const declared = deps[constraint.package];
602
+ if (!declared) continue;
603
+ const result = checkNodeEngineConstraint(constraint, declared, serviceNodeRange);
604
+ if (!result.compatible && result.reason) {
605
+ violations.push(
606
+ makeViolation(
607
+ policy,
608
+ rule,
609
+ `${svcId}:node-engine:${constraint.package}@${declared}`,
610
+ result.reason,
611
+ { nodeId: svcId },
612
+ ctx
613
+ )
614
+ );
615
+ }
616
+ }
818
617
  }
819
- }
820
- const dependencies = [];
821
- const seenEdge = /* @__PURE__ */ new Set();
822
- let hasExtractedOutbound = false;
823
- for (const src of scope) {
824
- for (const edgeId of graph.outboundEdges(src)) {
825
- const e = graph.getEdgeAttributes(edgeId);
826
- if (e.type === EdgeType.CONTAINS) continue;
827
- if (e.provenance === Provenance.OBSERVED) {
828
- if (!seenEdge.has(e.id)) {
829
- seenEdge.add(e.id);
830
- dependencies.push(e);
618
+ if (wantsKind("package-conflict")) {
619
+ for (const conflict of packageConflicts()) {
620
+ const declared = deps[conflict.package];
621
+ if (!declared) continue;
622
+ const requiredDeclared = deps[conflict.requires.name];
623
+ const result = checkPackageConflict(conflict, declared, requiredDeclared);
624
+ if (!result.compatible && result.reason) {
625
+ violations.push(
626
+ makeViolation(
627
+ policy,
628
+ rule,
629
+ `${svcId}:package-conflict:${conflict.package}@${declared}`,
630
+ result.reason,
631
+ { nodeId: svcId },
632
+ ctx
633
+ )
634
+ );
831
635
  }
832
- } else if (e.provenance === Provenance.EXTRACTED) {
833
- hasExtractedOutbound = true;
834
636
  }
835
637
  }
836
- }
837
- let inboundObservedCount = 0;
838
- for (const tgt of scope) {
839
- for (const edgeId of graph.inboundEdges(tgt)) {
840
- const e = graph.getEdgeAttributes(edgeId);
841
- if (e.type === EdgeType.CONTAINS) continue;
842
- if (e.provenance === Provenance.OBSERVED) inboundObservedCount += 1;
638
+ if (wantsKind("deprecated-api")) {
639
+ for (const dep of deprecatedApis()) {
640
+ const declared = deps[dep.package];
641
+ if (!declared) continue;
642
+ const result = checkDeprecatedApi(dep, declared);
643
+ if (!result.compatible && result.reason) {
644
+ violations.push(
645
+ makeViolation(
646
+ policy,
647
+ rule,
648
+ `${svcId}:deprecated-api:${dep.package}@${declared}`,
649
+ result.reason,
650
+ { nodeId: svcId },
651
+ ctx
652
+ )
653
+ );
654
+ }
655
+ }
843
656
  }
844
- }
845
- dependencies.sort(
846
- (a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
847
- );
848
- return ObservedDependenciesResultSchema.parse({
849
- origin: nodeId,
850
- dependencies,
851
- observed: dependencies.length > 0 || inboundObservedCount > 0,
852
- inboundObservedCount,
853
- hasExtractedOutbound
854
657
  });
855
- }
856
-
857
- // src/ingest.ts
858
- import { promises as fs3, existsSync, readFileSync } from "fs";
859
- import path3 from "path";
860
- import * as sourceMapJs from "source-map-js";
861
-
862
- // src/policy.ts
863
- import { promises as fs2 } from "fs";
864
- import path2 from "path";
865
- import {
866
- EdgeType as EdgeType2,
867
- NodeType as NodeType2,
868
- PolicyFileSchema
869
- } from "@neat.is/types";
870
-
871
- // src/events.ts
872
- import { EventEmitter } from "events";
873
- var EVENT_BUS_CHANNEL = "event";
874
- var NeatEventBus = class extends EventEmitter {
658
+ return violations;
875
659
  };
876
- var eventBus = new NeatEventBus();
877
- eventBus.setMaxListeners(0);
878
- function emitNeatEvent(envelope) {
879
- eventBus.emit(EVENT_BUS_CHANNEL, envelope);
660
+ var policyEvaluators = {
661
+ structural: evaluateStructural,
662
+ ownership: evaluateOwnership,
663
+ provenance: evaluateProvenance,
664
+ "blast-radius": evaluateBlastRadius,
665
+ compatibility: evaluateCompatibility
666
+ };
667
+ function canPromoteFrontier(graph, frontierId2, policies, ctx) {
668
+ if (policies.length === 0) return { allowed: true, violations: [] };
669
+ const all = evaluateAllPolicies(graph, policies, ctx);
670
+ const blocking = all.filter((v) => {
671
+ if (v.onViolation !== "block") return false;
672
+ return v.subject.nodeId === frontierId2 || v.subject.path?.includes(frontierId2) === true;
673
+ });
674
+ return { allowed: blocking.length === 0, violations: blocking };
880
675
  }
881
- function attachGraphToEventBus(graph, opts) {
882
- const { project } = opts;
883
- const onNodeAdded = (payload) => {
884
- emitNeatEvent({
885
- type: "node-added",
886
- project,
887
- payload: { node: payload.attributes }
888
- });
889
- };
890
- const onNodeDropped = (payload) => {
891
- emitNeatEvent({
892
- type: "node-removed",
893
- project,
894
- payload: { id: payload.key }
895
- });
896
- };
897
- const onEdgeAdded = (payload) => {
898
- emitNeatEvent({
899
- type: "edge-added",
900
- project,
901
- payload: { edge: payload.attributes }
902
- });
903
- };
904
- const onEdgeDropped = (payload) => {
905
- emitNeatEvent({
906
- type: "edge-removed",
907
- project,
908
- payload: { id: payload.key }
909
- });
910
- };
911
- const onNodeAttrsUpdated = (payload) => {
912
- emitNeatEvent({
913
- type: "node-updated",
914
- project,
915
- payload: { id: payload.key, changes: payload.attributes }
676
+ function evaluateAllPolicies(graph, policies, ctx) {
677
+ const out = [];
678
+ for (const policy of policies) {
679
+ const evaluator = policyEvaluators[policy.rule.type];
680
+ const violations = evaluator({ graph, policy, rule: policy.rule, ctx });
681
+ for (const v of violations) out.push(v);
682
+ }
683
+ return out;
684
+ }
685
+ function selectApplicablePolicies(graph, policies, nodeId) {
686
+ if (!graph.hasNode(nodeId)) return [];
687
+ const node = graph.getNodeAttributes(nodeId);
688
+ const out = [];
689
+ for (const policy of policies) {
690
+ const m = matchPolicyToNode(graph, policy, nodeId, node);
691
+ if (!m) continue;
692
+ out.push({
693
+ policyId: policy.id,
694
+ policyName: policy.name,
695
+ ...policy.description !== void 0 ? { description: policy.description } : {},
696
+ severity: policy.severity,
697
+ onViolation: resolveOnViolation(policy),
698
+ ruleType: policy.rule.type,
699
+ match: m.match,
700
+ reason: m.reason
916
701
  });
917
- };
918
- graph.on("nodeAdded", onNodeAdded);
919
- graph.on("nodeDropped", onNodeDropped);
920
- graph.on("edgeAdded", onEdgeAdded);
921
- graph.on("edgeDropped", onEdgeDropped);
922
- graph.on("nodeAttributesUpdated", onNodeAttrsUpdated);
923
- return () => {
924
- graph.off("nodeAdded", onNodeAdded);
925
- graph.off("nodeDropped", onNodeDropped);
926
- graph.off("edgeAdded", onEdgeAdded);
927
- graph.off("edgeDropped", onEdgeDropped);
928
- graph.off("nodeAttributesUpdated", onNodeAttrsUpdated);
929
- };
702
+ }
703
+ return out;
930
704
  }
931
-
932
- // src/policy.ts
933
- var DEFAULT_ACTION_BY_SEVERITY = {
934
- info: "log",
935
- warning: "alert",
936
- error: "alert",
937
- critical: "block"
938
- };
939
- function resolveOnViolation(policy) {
940
- return policy.onViolation ?? DEFAULT_ACTION_BY_SEVERITY[policy.severity];
705
+ function requiredProvenanceList(required) {
706
+ if (Array.isArray(required)) return required.join(" | ");
707
+ return String(required);
941
708
  }
942
- function makeViolation(policy, rule, contextSuffix, message, subject, ctx) {
943
- return {
944
- id: `${policy.id}:${contextSuffix}`,
945
- policyId: policy.id,
946
- policyName: policy.name,
947
- severity: policy.severity,
948
- onViolation: resolveOnViolation(policy),
949
- ruleType: rule.type,
950
- subject,
951
- message,
952
- observedAt: new Date(ctx.now()).toISOString()
953
- };
709
+ function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
710
+ const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
711
+ for (const edgeId of incident) {
712
+ const e = graph.getEdgeAttributes(edgeId);
713
+ if (e.type !== edgeType) continue;
714
+ if (requiredOtherEnd === void 0) return true;
715
+ if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
716
+ }
717
+ return false;
954
718
  }
955
- var evaluateStructural = ({
956
- graph,
957
- policy,
958
- rule,
959
- ctx
960
- }) => {
961
- const violations = [];
962
- graph.forEachNode((id, attrs) => {
963
- const a = attrs;
964
- if (a.type !== rule.fromNodeType) return;
965
- let satisfied = false;
966
- for (const edgeId of graph.outboundEdges(id)) {
967
- const e = graph.getEdgeAttributes(edgeId);
968
- if (e.type !== rule.edgeType) continue;
969
- const target = graph.getNodeAttributes(e.target);
970
- if (target.type === NodeType2.FrontierNode) continue;
971
- if (target.type === rule.toNodeType) {
972
- satisfied = true;
973
- break;
719
+ function blastRadiusSubjectReaching(graph, rule, nodeId) {
720
+ let found = null;
721
+ graph.forEachNode((subjId, attrs) => {
722
+ if (found !== null) return;
723
+ if (subjId === nodeId) return;
724
+ if (attrs.type !== rule.nodeType) return;
725
+ const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
726
+ if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
727
+ });
728
+ return found;
729
+ }
730
+ function matchPolicyToNode(graph, policy, nodeId, node) {
731
+ const rule = policy.rule;
732
+ switch (rule.type) {
733
+ case "structural": {
734
+ if (node.type === rule.fromNodeType) {
735
+ return {
736
+ match: "subject",
737
+ reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
738
+ };
739
+ }
740
+ if (node.type === rule.toNodeType) {
741
+ return {
742
+ match: "region",
743
+ reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
744
+ };
974
745
  }
746
+ return null;
975
747
  }
976
- if (!satisfied) {
977
- violations.push(
978
- makeViolation(
979
- policy,
980
- rule,
981
- id,
982
- `${rule.fromNodeType} ${id} has no ${rule.edgeType} edge to a ${rule.toNodeType}`,
983
- { nodeId: id },
984
- ctx
985
- )
986
- );
748
+ case "ownership": {
749
+ if (node.type === rule.nodeType) {
750
+ return {
751
+ match: "subject",
752
+ reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
753
+ };
754
+ }
755
+ return null;
987
756
  }
988
- });
989
- return violations;
990
- };
991
- var evaluateOwnership = ({
992
- graph,
993
- policy,
994
- rule,
995
- ctx
996
- }) => {
997
- const violations = [];
998
- graph.forEachNode((id, attrs) => {
999
- const a = attrs;
1000
- if (a.type !== rule.nodeType) return;
1001
- const value = a[rule.field];
1002
- if (typeof value !== "string" || value.length === 0) {
1003
- violations.push(
1004
- makeViolation(
1005
- policy,
1006
- rule,
1007
- id,
1008
- `${rule.nodeType} ${id} is missing required field "${rule.field}"`,
1009
- { nodeId: id },
1010
- ctx
1011
- )
1012
- );
757
+ case "blast-radius": {
758
+ const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
759
+ if (node.type === rule.nodeType) {
760
+ return {
761
+ match: "subject",
762
+ reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
763
+ };
764
+ }
765
+ const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
766
+ if (subject) {
767
+ return {
768
+ match: "region",
769
+ reason: `this node is in the blast radius of ${subject} (a ${rule.nodeType} held to a blast radius of ${rule.maxAffected}${depthLabel}) \u2014 it breaks if that ${rule.nodeType} changes`
770
+ };
771
+ }
772
+ return null;
1013
773
  }
1014
- });
1015
- return violations;
1016
- };
1017
- var evaluateProvenance = ({
1018
- graph,
1019
- policy,
1020
- rule,
1021
- ctx
1022
- }) => {
1023
- const required = Array.isArray(rule.required) ? new Set(rule.required) : /* @__PURE__ */ new Set([rule.required]);
1024
- const violations = [];
1025
- graph.forEachEdge((edgeId, attrs) => {
1026
- const e = attrs;
1027
- if (e.type !== rule.edgeType) return;
1028
- if (rule.targetNodeId && e.target !== rule.targetNodeId) return;
1029
- if (!required.has(e.provenance)) {
1030
- const requiredList = [...required].join(" | ");
1031
- violations.push(
1032
- makeViolation(
1033
- policy,
1034
- rule,
1035
- edgeId,
1036
- `${rule.edgeType} edge ${edgeId} has provenance ${e.provenance}; required ${requiredList}`,
1037
- { edgeId },
1038
- ctx
1039
- )
1040
- );
1041
- }
1042
- });
1043
- return violations;
1044
- };
1045
- var evaluateBlastRadius = ({
1046
- graph,
1047
- policy,
1048
- rule,
1049
- ctx
1050
- }) => {
1051
- const violations = [];
1052
- const depth = rule.depth;
1053
- graph.forEachNode((id, attrs) => {
1054
- const a = attrs;
1055
- if (a.type !== rule.nodeType) return;
1056
- const result = depth !== void 0 ? getBlastRadius(graph, id, depth) : getBlastRadius(graph, id);
1057
- if (result.totalAffected > rule.maxAffected) {
1058
- violations.push(
1059
- makeViolation(
1060
- policy,
1061
- rule,
1062
- id,
1063
- `${rule.nodeType} ${id} has blast radius ${result.totalAffected} > ${rule.maxAffected}`,
1064
- { nodeId: id, path: [id] },
1065
- ctx
1066
- )
1067
- );
1068
- }
1069
- });
1070
- return violations;
1071
- };
1072
- var evaluateCompatibility = ({
1073
- graph,
1074
- policy,
1075
- rule,
1076
- ctx
1077
- }) => {
1078
- const violations = [];
1079
- const wantsKind = (kind) => rule.kind === void 0 || rule.kind === kind;
1080
- graph.forEachNode((svcId, attrs) => {
1081
- const a = attrs;
1082
- if (a.type !== NodeType2.ServiceNode) return;
1083
- const svc = a;
1084
- const deps = svc.dependencies ?? {};
1085
- if (wantsKind("driver-engine")) {
1086
- for (const edgeId of graph.outboundEdges(svcId)) {
1087
- const e = graph.getEdgeAttributes(edgeId);
1088
- if (e.type !== EdgeType2.CONNECTS_TO) continue;
1089
- const dbAttrs = graph.getNodeAttributes(e.target);
1090
- if (dbAttrs.type === NodeType2.FrontierNode) continue;
1091
- if (dbAttrs.type !== NodeType2.DatabaseNode) continue;
1092
- const db = dbAttrs;
1093
- for (const pair of compatPairs()) {
1094
- if (pair.engine !== db.engine) continue;
1095
- const declared = deps[pair.driver];
1096
- if (!declared) continue;
1097
- const result = checkCompatibility(pair.driver, declared, db.engine, db.engineVersion);
1098
- if (!result.compatible && result.reason) {
1099
- violations.push(
1100
- makeViolation(
1101
- policy,
1102
- rule,
1103
- `${svcId}:driver-engine:${pair.driver}@${declared}:${db.engine}@${db.engineVersion}`,
1104
- result.reason,
1105
- { nodeId: svcId, edgeId },
1106
- ctx
1107
- )
1108
- );
1109
- }
1110
- }
1111
- }
1112
- }
1113
- if (wantsKind("node-engine")) {
1114
- const serviceNodeRange = svc.nodeEngine;
1115
- for (const constraint of nodeEngineConstraints()) {
1116
- const declared = deps[constraint.package];
1117
- if (!declared) continue;
1118
- const result = checkNodeEngineConstraint(constraint, declared, serviceNodeRange);
1119
- if (!result.compatible && result.reason) {
1120
- violations.push(
1121
- makeViolation(
1122
- policy,
1123
- rule,
1124
- `${svcId}:node-engine:${constraint.package}@${declared}`,
1125
- result.reason,
1126
- { nodeId: svcId },
1127
- ctx
1128
- )
1129
- );
1130
- }
1131
- }
1132
- }
1133
- if (wantsKind("package-conflict")) {
1134
- for (const conflict of packageConflicts()) {
1135
- const declared = deps[conflict.package];
1136
- if (!declared) continue;
1137
- const requiredDeclared = deps[conflict.requires.name];
1138
- const result = checkPackageConflict(conflict, declared, requiredDeclared);
1139
- if (!result.compatible && result.reason) {
1140
- violations.push(
1141
- makeViolation(
1142
- policy,
1143
- rule,
1144
- `${svcId}:package-conflict:${conflict.package}@${declared}`,
1145
- result.reason,
1146
- { nodeId: svcId },
1147
- ctx
1148
- )
1149
- );
1150
- }
1151
- }
1152
- }
1153
- if (wantsKind("deprecated-api")) {
1154
- for (const dep of deprecatedApis()) {
1155
- const declared = deps[dep.package];
1156
- if (!declared) continue;
1157
- const result = checkDeprecatedApi(dep, declared);
1158
- if (!result.compatible && result.reason) {
1159
- violations.push(
1160
- makeViolation(
1161
- policy,
1162
- rule,
1163
- `${svcId}:deprecated-api:${dep.package}@${declared}`,
1164
- result.reason,
1165
- { nodeId: svcId },
1166
- ctx
1167
- )
1168
- );
1169
- }
1170
- }
1171
- }
1172
- });
1173
- return violations;
1174
- };
1175
- var policyEvaluators = {
1176
- structural: evaluateStructural,
1177
- ownership: evaluateOwnership,
1178
- provenance: evaluateProvenance,
1179
- "blast-radius": evaluateBlastRadius,
1180
- compatibility: evaluateCompatibility
1181
- };
1182
- function canPromoteFrontier(graph, frontierId2, policies, ctx) {
1183
- if (policies.length === 0) return { allowed: true, violations: [] };
1184
- const all = evaluateAllPolicies(graph, policies, ctx);
1185
- const blocking = all.filter((v) => {
1186
- if (v.onViolation !== "block") return false;
1187
- return v.subject.nodeId === frontierId2 || v.subject.path?.includes(frontierId2) === true;
1188
- });
1189
- return { allowed: blocking.length === 0, violations: blocking };
1190
- }
1191
- function evaluateAllPolicies(graph, policies, ctx) {
1192
- const out = [];
1193
- for (const policy of policies) {
1194
- const evaluator = policyEvaluators[policy.rule.type];
1195
- const violations = evaluator({ graph, policy, rule: policy.rule, ctx });
1196
- for (const v of violations) out.push(v);
1197
- }
1198
- return out;
1199
- }
1200
- function selectApplicablePolicies(graph, policies, nodeId) {
1201
- if (!graph.hasNode(nodeId)) return [];
1202
- const node = graph.getNodeAttributes(nodeId);
1203
- const out = [];
1204
- for (const policy of policies) {
1205
- const m = matchPolicyToNode(graph, policy, nodeId, node);
1206
- if (!m) continue;
1207
- out.push({
1208
- policyId: policy.id,
1209
- policyName: policy.name,
1210
- ...policy.description !== void 0 ? { description: policy.description } : {},
1211
- severity: policy.severity,
1212
- onViolation: resolveOnViolation(policy),
1213
- ruleType: policy.rule.type,
1214
- match: m.match,
1215
- reason: m.reason
1216
- });
1217
- }
1218
- return out;
1219
- }
1220
- function requiredProvenanceList(required) {
1221
- if (Array.isArray(required)) return required.join(" | ");
1222
- return String(required);
1223
- }
1224
- function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
1225
- const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
1226
- for (const edgeId of incident) {
1227
- const e = graph.getEdgeAttributes(edgeId);
1228
- if (e.type !== edgeType) continue;
1229
- if (requiredOtherEnd === void 0) return true;
1230
- if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
1231
- }
1232
- return false;
1233
- }
1234
- function blastRadiusSubjectReaching(graph, rule, nodeId) {
1235
- let found = null;
1236
- graph.forEachNode((subjId, attrs) => {
1237
- if (found !== null) return;
1238
- if (subjId === nodeId) return;
1239
- if (attrs.type !== rule.nodeType) return;
1240
- const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
1241
- if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
1242
- });
1243
- return found;
1244
- }
1245
- function matchPolicyToNode(graph, policy, nodeId, node) {
1246
- const rule = policy.rule;
1247
- switch (rule.type) {
1248
- case "structural": {
1249
- if (node.type === rule.fromNodeType) {
1250
- return {
1251
- match: "subject",
1252
- reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
1253
- };
1254
- }
1255
- if (node.type === rule.toNodeType) {
1256
- return {
1257
- match: "region",
1258
- reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
1259
- };
1260
- }
1261
- return null;
1262
- }
1263
- case "ownership": {
1264
- if (node.type === rule.nodeType) {
1265
- return {
1266
- match: "subject",
1267
- reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
1268
- };
1269
- }
1270
- return null;
1271
- }
1272
- case "blast-radius": {
1273
- const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
1274
- if (node.type === rule.nodeType) {
1275
- return {
1276
- match: "subject",
1277
- reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
1278
- };
1279
- }
1280
- const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
1281
- if (subject) {
1282
- return {
1283
- match: "region",
1284
- reason: `this node is in the blast radius of ${subject} (a ${rule.nodeType} held to a blast radius of ${rule.maxAffected}${depthLabel}) \u2014 it breaks if that ${rule.nodeType} changes`
1285
- };
1286
- }
1287
- return null;
1288
- }
1289
- case "compatibility": {
1290
- const kindLabel = rule.kind ?? "all compat shapes";
1291
- if (node.type === NodeType2.ServiceNode) {
1292
- return {
1293
- match: "subject",
1294
- reason: `this service's dependencies are compatibility-checked (${kindLabel})`
1295
- };
1296
- }
1297
- const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
1298
- if (reachesDriverEngine && node.type === NodeType2.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, EdgeType2.CONNECTS_TO)) {
1299
- return {
1300
- match: "region",
1301
- reason: "services connecting to this database have their driver/engine compatibility checked against it"
1302
- };
1303
- }
1304
- return null;
774
+ case "compatibility": {
775
+ const kindLabel = rule.kind ?? "all compat shapes";
776
+ if (node.type === NodeType.ServiceNode) {
777
+ return {
778
+ match: "subject",
779
+ reason: `this service's dependencies are compatibility-checked (${kindLabel})`
780
+ };
781
+ }
782
+ const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
783
+ if (reachesDriverEngine && node.type === NodeType.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, EdgeType.CONNECTS_TO)) {
784
+ return {
785
+ match: "region",
786
+ reason: "services connecting to this database have their driver/engine compatibility checked against it"
787
+ };
788
+ }
789
+ return null;
1305
790
  }
1306
791
  case "provenance": {
1307
792
  const requiredList = requiredProvenanceList(rule.required);
@@ -1372,11 +857,11 @@ var PolicyViolationsLog = class {
1372
857
 
1373
858
  // src/ingest.ts
1374
859
  import {
1375
- EdgeType as EdgeType3,
860
+ EdgeType as EdgeType2,
1376
861
  GraphEdgeSchema,
1377
862
  GraphNodeSchema,
1378
- NodeType as NodeType3,
1379
- Provenance as Provenance2,
863
+ NodeType as NodeType2,
864
+ Provenance,
1380
865
  confidenceForObservedSignal,
1381
866
  databaseId,
1382
867
  localDatabaseId,
@@ -1560,9 +1045,30 @@ function isLoopbackHost(host) {
1560
1045
  const h = host.toLowerCase();
1561
1046
  return h === "localhost" || h === "ip6-localhost" || h === "::1" || h === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(h);
1562
1047
  }
1048
+ var CODE_FILE_PATH_ATTR = "code.file.path";
1563
1049
  var CODE_FILEPATH_ATTR = "code.filepath";
1050
+ var CODE_LINE_NUMBER_ATTR = "code.line.number";
1564
1051
  var CODE_LINENO_ATTR = "code.lineno";
1052
+ var CODE_FUNCTION_NAME_ATTR = "code.function.name";
1565
1053
  var CODE_FUNCTION_ATTR = "code.function";
1054
+ function codeFilepathOf(attrs) {
1055
+ const stable = attrs[CODE_FILE_PATH_ATTR];
1056
+ if (typeof stable === "string" && stable.length > 0) return stable;
1057
+ const prior = attrs[CODE_FILEPATH_ATTR];
1058
+ return typeof prior === "string" && prior.length > 0 ? prior : void 0;
1059
+ }
1060
+ function codeLinenoOf(attrs) {
1061
+ const stable = attrs[CODE_LINE_NUMBER_ATTR];
1062
+ if (typeof stable === "number" && Number.isFinite(stable)) return stable;
1063
+ const prior = attrs[CODE_LINENO_ATTR];
1064
+ return typeof prior === "number" && Number.isFinite(prior) ? prior : void 0;
1065
+ }
1066
+ function codeFunctionOf(attrs) {
1067
+ const stable = attrs[CODE_FUNCTION_NAME_ATTR];
1068
+ if (typeof stable === "string" && stable.length > 0) return stable;
1069
+ const prior = attrs[CODE_FUNCTION_ATTR];
1070
+ return typeof prior === "string" && prior.length > 0 ? prior : void 0;
1071
+ }
1566
1072
  function toPosix(p) {
1567
1073
  return p.split("\\").join("/");
1568
1074
  }
@@ -1640,10 +1146,9 @@ function resolveDistToSrc(absFilepath, line) {
1640
1146
  }
1641
1147
  }
1642
1148
  function callSiteFromSpan(span, serviceNode, scanPath) {
1643
- const filepath = span.attributes[CODE_FILEPATH_ATTR];
1644
- if (typeof filepath !== "string" || filepath.length === 0) return null;
1645
- const linenoRaw = span.attributes[CODE_LINENO_ATTR];
1646
- let line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1149
+ const filepath = codeFilepathOf(span.attributes);
1150
+ if (filepath === void 0) return null;
1151
+ let line = codeLinenoOf(span.attributes);
1647
1152
  const abs = toPosix(filepath).replace(/^file:\/\//, "");
1648
1153
  const resolved = resolveDistToSrc(abs, line);
1649
1154
  let effectivePath = filepath;
@@ -1658,8 +1163,7 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
1658
1163
  if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && serviceNode?.name) {
1659
1164
  warnNoSourceMaps(serviceNode.name);
1660
1165
  }
1661
- const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
1662
- const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
1166
+ const fn = codeFunctionOf(span.attributes);
1663
1167
  return {
1664
1168
  relPath,
1665
1169
  ...line !== void 0 ? { line } : {},
@@ -1672,7 +1176,7 @@ function reconcileObservedRelPath(graph, serviceName, relPath) {
1672
1176
  let best = null;
1673
1177
  graph.forEachNode((_id, attrs) => {
1674
1178
  const a = attrs;
1675
- if (a.type !== NodeType3.FileNode || a.service !== serviceName) return;
1179
+ if (a.type !== NodeType2.FileNode || a.service !== serviceName) return;
1676
1180
  if (a.discoveredVia === "otel") return;
1677
1181
  const p = a.path;
1678
1182
  if (!p) return;
@@ -1689,7 +1193,7 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1689
1193
  const language = languageForExt(relPath);
1690
1194
  const node = {
1691
1195
  id: fileNodeId,
1692
- type: NodeType3.FileNode,
1196
+ type: NodeType2.FileNode,
1693
1197
  service: serviceName,
1694
1198
  path: relPath,
1695
1199
  ...language ? { language } : {},
@@ -1698,14 +1202,14 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1698
1202
  };
1699
1203
  graph.addNode(fileNodeId, node);
1700
1204
  }
1701
- const containsId = makeObservedEdgeId(EdgeType3.CONTAINS, serviceNodeId, fileNodeId);
1205
+ const containsId = makeObservedEdgeId(EdgeType2.CONTAINS, serviceNodeId, fileNodeId);
1702
1206
  if (!graph.hasEdge(containsId)) {
1703
1207
  const edge = {
1704
1208
  id: containsId,
1705
1209
  source: serviceNodeId,
1706
1210
  target: fileNodeId,
1707
- type: EdgeType3.CONTAINS,
1708
- provenance: Provenance2.OBSERVED
1211
+ type: EdgeType2.CONTAINS,
1212
+ provenance: Provenance.OBSERVED
1709
1213
  };
1710
1214
  graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
1711
1215
  }
@@ -1720,9 +1224,9 @@ function makeInferredEdgeId(type, source, target) {
1720
1224
  var INFERRED_CONFIDENCE = 0.6;
1721
1225
  var STITCH_MAX_DEPTH = 2;
1722
1226
  var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
1723
- EdgeType3.CALLS,
1724
- EdgeType3.CONNECTS_TO,
1725
- EdgeType3.DEPENDS_ON
1227
+ EdgeType2.CALLS,
1228
+ EdgeType2.CONNECTS_TO,
1229
+ EdgeType2.DEPENDS_ON
1726
1230
  ]);
1727
1231
  var WIRE_SPAN_KIND_CLIENT = 3;
1728
1232
  var WIRE_SPAN_KIND_PRODUCER = 4;
@@ -1742,7 +1246,7 @@ function ensureGraphqlOperationNode(graph, serviceName, operationType, operation
1742
1246
  if (graph.hasNode(id)) return id;
1743
1247
  const node = {
1744
1248
  id,
1745
- type: NodeType3.GraphQLOperationNode,
1249
+ type: NodeType2.GraphQLOperationNode,
1746
1250
  name: operationName,
1747
1251
  service: serviceName,
1748
1252
  operationType: operationType.toLowerCase(),
@@ -1760,7 +1264,7 @@ function ensureGrpcMethodNode(graph, rpcService, rpcMethod) {
1760
1264
  if (graph.hasNode(id)) return id;
1761
1265
  const node = {
1762
1266
  id,
1763
- type: NodeType3.GrpcMethodNode,
1267
+ type: NodeType2.GrpcMethodNode,
1764
1268
  name: `${rpcService}/${rpcMethod}`,
1765
1269
  rpcService,
1766
1270
  rpcMethod,
@@ -1777,7 +1281,7 @@ function ensureWebsocketChannelNode(graph, serviceName, channel) {
1777
1281
  if (graph.hasNode(id)) return id;
1778
1282
  const node = {
1779
1283
  id,
1780
- type: NodeType3.WebSocketChannelNode,
1284
+ type: NodeType2.WebSocketChannelNode,
1781
1285
  name: channel,
1782
1286
  service: serviceName,
1783
1287
  channel,
@@ -1794,7 +1298,7 @@ function ensureMessagingDestinationNode(graph, system, destination) {
1794
1298
  if (graph.hasNode(id)) return id;
1795
1299
  const node = {
1796
1300
  id,
1797
- type: NodeType3.InfraNode,
1301
+ type: NodeType2.InfraNode,
1798
1302
  name: destination,
1799
1303
  provider: "self",
1800
1304
  kind: messagingDestinationKind(system)
@@ -1848,7 +1352,7 @@ function resolveServiceId(graph, host, env) {
1848
1352
  graph.forEachNode((id, attrs) => {
1849
1353
  if (sameEnv) return;
1850
1354
  const a = attrs;
1851
- if (a.type !== NodeType3.ServiceNode) return;
1355
+ if (a.type !== NodeType2.ServiceNode) return;
1852
1356
  const matchesByName = a.name === host;
1853
1357
  const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
1854
1358
  if (!matchesByName && !matchesByAlias) return;
@@ -1870,7 +1374,7 @@ function ensureServiceNode(graph, serviceName, env) {
1870
1374
  if (graph.hasNode(id)) return id;
1871
1375
  const node = {
1872
1376
  id,
1873
- type: NodeType3.ServiceNode,
1377
+ type: NodeType2.ServiceNode,
1874
1378
  name: serviceName,
1875
1379
  language: "unknown",
1876
1380
  discoveredVia: "otel",
@@ -1884,7 +1388,7 @@ function ensureInfraNode(graph, kind, name, provider) {
1884
1388
  if (graph.hasNode(id)) return id;
1885
1389
  const node = {
1886
1390
  id,
1887
- type: NodeType3.InfraNode,
1391
+ type: NodeType2.InfraNode,
1888
1392
  name,
1889
1393
  provider,
1890
1394
  kind
@@ -1897,7 +1401,7 @@ function ensureDatabaseNode(graph, host, engine) {
1897
1401
  if (graph.hasNode(id)) return id;
1898
1402
  const node = {
1899
1403
  id,
1900
- type: NodeType3.DatabaseNode,
1404
+ type: NodeType2.DatabaseNode,
1901
1405
  name: host,
1902
1406
  engine,
1903
1407
  engineVersion: "unknown",
@@ -1913,7 +1417,7 @@ function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
1913
1417
  if (graph.hasNode(id)) return id;
1914
1418
  const node = {
1915
1419
  id,
1916
- type: NodeType3.DatabaseNode,
1420
+ type: NodeType2.DatabaseNode,
1917
1421
  name,
1918
1422
  engine,
1919
1423
  engineVersion: "unknown",
@@ -1928,17 +1432,17 @@ function findDeclaredDatabaseForService(graph, serviceNodeId, engine) {
1928
1432
  const sources = [serviceNodeId];
1929
1433
  for (const edgeId of graph.outboundEdges(serviceNodeId)) {
1930
1434
  const e = graph.getEdgeAttributes(edgeId);
1931
- if (e.type === EdgeType3.CONTAINS) sources.push(e.target);
1435
+ if (e.type === EdgeType2.CONTAINS) sources.push(e.target);
1932
1436
  }
1933
1437
  const matches = /* @__PURE__ */ new Set();
1934
1438
  for (const src of sources) {
1935
1439
  if (!graph.hasNode(src)) continue;
1936
1440
  for (const edgeId of graph.outboundEdges(src)) {
1937
1441
  const edge = graph.getEdgeAttributes(edgeId);
1938
- if (edge.type !== EdgeType3.CONNECTS_TO || edge.provenance !== Provenance2.EXTRACTED) continue;
1442
+ if (edge.type !== EdgeType2.CONNECTS_TO || edge.provenance !== Provenance.EXTRACTED) continue;
1939
1443
  if (!graph.hasNode(edge.target)) continue;
1940
1444
  const target = graph.getNodeAttributes(edge.target);
1941
- if (target.type !== NodeType3.DatabaseNode || target.engine !== engine) continue;
1445
+ if (target.type !== NodeType2.DatabaseNode || target.engine !== engine) continue;
1942
1446
  matches.add(edge.target);
1943
1447
  }
1944
1448
  }
@@ -1953,7 +1457,7 @@ function ensureFrontierNode(graph, host, ts) {
1953
1457
  }
1954
1458
  const node = {
1955
1459
  id,
1956
- type: NodeType3.FrontierNode,
1460
+ type: NodeType2.FrontierNode,
1957
1461
  name: host,
1958
1462
  host,
1959
1463
  firstObserved: ts,
@@ -1977,7 +1481,7 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
1977
1481
  };
1978
1482
  const updated = {
1979
1483
  ...existing,
1980
- provenance: Provenance2.OBSERVED,
1484
+ provenance: Provenance.OBSERVED,
1981
1485
  lastObserved: ts,
1982
1486
  callCount: newSpanCount,
1983
1487
  signal: newSignal,
@@ -1998,7 +1502,7 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
1998
1502
  source,
1999
1503
  target,
2000
1504
  type,
2001
- provenance: Provenance2.OBSERVED,
1505
+ provenance: Provenance.OBSERVED,
2002
1506
  confidence: confidenceForObservedSignal(signal),
2003
1507
  lastObserved: ts,
2004
1508
  callCount: 1,
@@ -2021,7 +1525,7 @@ function stitchTrace(graph, sourceServiceId, ts) {
2021
1525
  const outbound = graph.outboundEdges(nodeId);
2022
1526
  for (const edgeId of outbound) {
2023
1527
  const edge = graph.getEdgeAttributes(edgeId);
2024
- if (edge.provenance !== Provenance2.EXTRACTED) continue;
1528
+ if (edge.provenance !== Provenance.EXTRACTED) continue;
2025
1529
  if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
2026
1530
  if (graph.hasEdge(observedEdgeId(edge.source, edge.target, edge.type))) continue;
2027
1531
  upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
@@ -2045,7 +1549,7 @@ function upsertInferredEdge(graph, type, source, target, ts) {
2045
1549
  source,
2046
1550
  target,
2047
1551
  type,
2048
- provenance: Provenance2.INFERRED,
1552
+ provenance: Provenance.INFERRED,
2049
1553
  confidence: INFERRED_CONFIDENCE,
2050
1554
  lastObserved: ts
2051
1555
  };
@@ -2221,7 +1725,7 @@ async function handleSpan(ctx, span) {
2221
1725
  }
2222
1726
  const result = upsertObservedEdge(
2223
1727
  ctx.graph,
2224
- EdgeType3.CONNECTS_TO,
1728
+ EdgeType2.CONNECTS_TO,
2225
1729
  observedSource(),
2226
1730
  targetId,
2227
1731
  ts,
@@ -2233,7 +1737,7 @@ async function handleSpan(ctx, span) {
2233
1737
  const collectionId = ensureInfraNode(ctx.graph, "mongodb-collection", span.dbCollection, "self");
2234
1738
  upsertObservedEdge(
2235
1739
  ctx.graph,
2236
- EdgeType3.CALLS,
1740
+ EdgeType2.CALLS,
2237
1741
  observedSource(),
2238
1742
  collectionId,
2239
1743
  ts,
@@ -2241,6 +1745,18 @@ async function handleSpan(ctx, span) {
2241
1745
  callSiteEvidence
2242
1746
  );
2243
1747
  }
1748
+ if (span.dbTable) {
1749
+ const tableId = ensureInfraNode(ctx.graph, "sql-table", span.dbTable, "self");
1750
+ upsertObservedEdge(
1751
+ ctx.graph,
1752
+ EdgeType2.CALLS,
1753
+ observedSource(),
1754
+ tableId,
1755
+ ts,
1756
+ isError,
1757
+ callSiteEvidence
1758
+ );
1759
+ }
2244
1760
  }
2245
1761
  } else if (span.messagingSystem && span.messagingDestination && spanMintsMessagingEdge(span.kind)) {
2246
1762
  const targetId = ensureMessagingDestinationNode(
@@ -2248,7 +1764,7 @@ async function handleSpan(ctx, span) {
2248
1764
  span.messagingSystem,
2249
1765
  span.messagingDestination
2250
1766
  );
2251
- const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? EdgeType3.CONSUMES_FROM : EdgeType3.PUBLISHES_TO;
1767
+ const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? EdgeType2.CONSUMES_FROM : EdgeType2.PUBLISHES_TO;
2252
1768
  const result = upsertObservedEdge(
2253
1769
  ctx.graph,
2254
1770
  edgeType,
@@ -2268,7 +1784,7 @@ async function handleSpan(ctx, span) {
2268
1784
  );
2269
1785
  const result = upsertObservedEdge(
2270
1786
  ctx.graph,
2271
- EdgeType3.CONTAINS,
1787
+ EdgeType2.CONTAINS,
2272
1788
  observedSource(),
2273
1789
  targetId,
2274
1790
  ts,
@@ -2280,7 +1796,7 @@ async function handleSpan(ctx, span) {
2280
1796
  const targetId = ensureGrpcMethodNode(ctx.graph, span.rpcService, span.rpcMethod);
2281
1797
  const result = upsertObservedEdge(
2282
1798
  ctx.graph,
2283
- EdgeType3.CONTAINS,
1799
+ EdgeType2.CONTAINS,
2284
1800
  observedSource(),
2285
1801
  targetId,
2286
1802
  ts,
@@ -2296,7 +1812,7 @@ async function handleSpan(ctx, span) {
2296
1812
  );
2297
1813
  const result = upsertObservedEdge(
2298
1814
  ctx.graph,
2299
- EdgeType3.CONNECTS_TO,
1815
+ EdgeType2.CONNECTS_TO,
2300
1816
  observedSource(),
2301
1817
  targetId,
2302
1818
  ts,
@@ -2312,7 +1828,7 @@ async function handleSpan(ctx, span) {
2312
1828
  if (targetId && targetId !== sourceId) {
2313
1829
  upsertObservedEdge(
2314
1830
  ctx.graph,
2315
- EdgeType3.CALLS,
1831
+ EdgeType2.CALLS,
2316
1832
  observedSource(),
2317
1833
  targetId,
2318
1834
  ts,
@@ -2325,7 +1841,7 @@ async function handleSpan(ctx, span) {
2325
1841
  const frontierNodeId = ensureFrontierNode(ctx.graph, host, ts);
2326
1842
  upsertObservedEdge(
2327
1843
  ctx.graph,
2328
- EdgeType3.CALLS,
1844
+ EdgeType2.CALLS,
2329
1845
  observedSource(),
2330
1846
  frontierNodeId,
2331
1847
  ts,
@@ -2351,7 +1867,7 @@ async function handleSpan(ctx, span) {
2351
1867
  } : void 0;
2352
1868
  upsertObservedEdge(
2353
1869
  ctx.graph,
2354
- EdgeType3.CALLS,
1870
+ EdgeType2.CALLS,
2355
1871
  fallbackSource,
2356
1872
  sourceId,
2357
1873
  ts,
@@ -2361,286 +1877,801 @@ async function handleSpan(ctx, span) {
2361
1877
  }
2362
1878
  }
2363
1879
  }
2364
- if (span.statusCode === 2) {
2365
- stitchTrace(ctx.graph, sourceId, ts);
2366
- if (ctx.writeErrorEventInline !== false) {
2367
- const attrs = sanitizeAttributes(span.attributes);
2368
- const ev = {
2369
- id: `${span.traceId}:${span.spanId}`,
2370
- timestamp: ts,
2371
- service: span.service,
2372
- traceId: span.traceId,
2373
- spanId: span.spanId,
2374
- errorMessage: incidentMessage(span),
2375
- ...span.exception?.type ? { exceptionType: span.exception.type } : {},
2376
- ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
2377
- ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
2378
- affectedNode
2379
- };
2380
- await appendErrorEvent(ctx, ev);
1880
+ if (span.statusCode === 2) {
1881
+ stitchTrace(ctx.graph, sourceId, ts);
1882
+ if (ctx.writeErrorEventInline !== false) {
1883
+ const attrs = sanitizeAttributes(span.attributes);
1884
+ const ev = {
1885
+ id: `${span.traceId}:${span.spanId}`,
1886
+ timestamp: ts,
1887
+ service: span.service,
1888
+ traceId: span.traceId,
1889
+ spanId: span.spanId,
1890
+ errorMessage: incidentMessage(span),
1891
+ ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1892
+ ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1893
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
1894
+ affectedNode
1895
+ };
1896
+ await appendErrorEvent(ctx, ev);
1897
+ }
1898
+ }
1899
+ if (span.statusCode !== 2) {
1900
+ const status = httpResponseStatus(span);
1901
+ if (span.exception) {
1902
+ await recordExceptionIncident(ctx, span, ts);
1903
+ } else if (status !== void 0 && status >= 500) {
1904
+ await recordFailingResponseIncident(ctx, span, sourceId, ts, status, 1);
1905
+ } else if (status !== void 0 && status >= 400 && spanMintsObservedEdge(span.kind)) {
1906
+ await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status);
1907
+ }
1908
+ }
1909
+ void affectedNode;
1910
+ if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
1911
+ }
1912
+ function promoteFrontierNodes(graph, opts = {}) {
1913
+ const aliasIndex = /* @__PURE__ */ new Map();
1914
+ graph.forEachNode((id, attrs) => {
1915
+ const a = attrs;
1916
+ if (a.type !== NodeType2.ServiceNode) return;
1917
+ aliasIndex.set(a.name, id);
1918
+ if (a.aliases) {
1919
+ for (const alias of a.aliases) aliasIndex.set(alias, id);
1920
+ }
1921
+ });
1922
+ const toPromote = [];
1923
+ graph.forEachNode((id, attrs) => {
1924
+ const a = attrs;
1925
+ if (a.type !== NodeType2.FrontierNode) return;
1926
+ const target = aliasIndex.get(a.host);
1927
+ if (!target) return;
1928
+ if (target === id) return;
1929
+ toPromote.push({ frontierId: id, serviceId: target });
1930
+ });
1931
+ let promoted = 0;
1932
+ for (const { frontierId: frontierId2, serviceId: serviceId5 } of toPromote) {
1933
+ if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
1934
+ const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
1935
+ if (!gate.allowed) {
1936
+ continue;
1937
+ }
1938
+ }
1939
+ rewireFrontierEdges(graph, frontierId2, serviceId5);
1940
+ graph.dropNode(frontierId2);
1941
+ promoted++;
1942
+ }
1943
+ return promoted;
1944
+ }
1945
+ function rewireFrontierEdges(graph, frontierId2, serviceId5) {
1946
+ const inbound = [...graph.inboundEdges(frontierId2)];
1947
+ const outbound = [...graph.outboundEdges(frontierId2)];
1948
+ for (const edgeId of inbound) {
1949
+ const edge = graph.getEdgeAttributes(edgeId);
1950
+ rebuildEdge(graph, edge, edge.source, serviceId5, edgeId);
1951
+ }
1952
+ for (const edgeId of outbound) {
1953
+ const edge = graph.getEdgeAttributes(edgeId);
1954
+ rebuildEdge(graph, edge, serviceId5, edge.target, edgeId);
1955
+ }
1956
+ }
1957
+ function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
1958
+ graph.dropEdge(oldEdgeId);
1959
+ const newId = edge.provenance === Provenance.OBSERVED ? observedEdgeId(newSource, newTarget, edge.type) : edge.provenance === Provenance.INFERRED ? inferredEdgeId(newSource, newTarget, edge.type) : extractedEdgeId(newSource, newTarget, edge.type);
1960
+ if (graph.hasEdge(newId)) {
1961
+ const existing = graph.getEdgeAttributes(newId);
1962
+ const merged = {
1963
+ ...existing,
1964
+ callCount: (existing.callCount ?? 0) + (edge.callCount ?? 0),
1965
+ lastObserved: pickLater(existing.lastObserved, edge.lastObserved)
1966
+ };
1967
+ graph.replaceEdgeAttributes(newId, merged);
1968
+ return;
1969
+ }
1970
+ const rebuilt = {
1971
+ ...edge,
1972
+ id: newId,
1973
+ source: newSource,
1974
+ target: newTarget
1975
+ };
1976
+ graph.addEdgeWithKey(newId, newSource, newTarget, rebuilt);
1977
+ }
1978
+ function pickLater(a, b) {
1979
+ if (!a) return b;
1980
+ if (!b) return a;
1981
+ return new Date(a).getTime() >= new Date(b).getTime() ? a : b;
1982
+ }
1983
+ function makeSpanHandler(ctx) {
1984
+ return (span) => handleSpan(ctx, span);
1985
+ }
1986
+ async function markStaleEdges(graph, options = {}) {
1987
+ const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
1988
+ const now = options.now ?? Date.now();
1989
+ const events = [];
1990
+ const project = options.project ?? DEFAULT_PROJECT;
1991
+ graph.forEachEdge((id, attrs) => {
1992
+ const e = attrs;
1993
+ if (e.provenance !== Provenance.OBSERVED) return;
1994
+ if (!e.lastObserved) return;
1995
+ const threshold = thresholdForEdgeType(e.type, thresholds);
1996
+ const age = now - new Date(e.lastObserved).getTime();
1997
+ if (age > threshold) {
1998
+ const updated = { ...e, provenance: Provenance.STALE, confidence: 0.3 };
1999
+ graph.replaceEdgeAttributes(id, updated);
2000
+ events.push({
2001
+ edgeId: id,
2002
+ source: e.source,
2003
+ target: e.target,
2004
+ edgeType: e.type,
2005
+ thresholdMs: threshold,
2006
+ ageMs: age,
2007
+ lastObserved: e.lastObserved,
2008
+ transitionedAt: new Date(now).toISOString()
2009
+ });
2010
+ emitNeatEvent({
2011
+ type: "stale-transition",
2012
+ project,
2013
+ payload: {
2014
+ edgeId: id,
2015
+ from: Provenance.OBSERVED,
2016
+ to: Provenance.STALE
2017
+ }
2018
+ });
2019
+ }
2020
+ });
2021
+ if (options.staleEventsPath && events.length > 0) {
2022
+ await appendStaleEvents(options.staleEventsPath, events);
2023
+ }
2024
+ return { count: events.length, events };
2025
+ }
2026
+ async function appendStaleEvents(staleEventsPath, events) {
2027
+ await fs3.mkdir(path3.dirname(staleEventsPath), { recursive: true });
2028
+ const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
2029
+ await fs3.appendFile(staleEventsPath, lines, "utf8");
2030
+ }
2031
+ async function readStaleEvents(staleEventsPath) {
2032
+ try {
2033
+ const raw = await fs3.readFile(staleEventsPath, "utf8");
2034
+ return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2035
+ } catch (err) {
2036
+ if (err.code === "ENOENT") return [];
2037
+ throw err;
2038
+ }
2039
+ }
2040
+ function startStalenessLoop(graph, options = {}) {
2041
+ let stopped = false;
2042
+ const intervalMs = options.intervalMs ?? 6e4;
2043
+ const tick = () => {
2044
+ if (stopped) return;
2045
+ void (async () => {
2046
+ try {
2047
+ await markStaleEdges(graph, {
2048
+ thresholds: options.thresholds,
2049
+ staleEventsPath: options.staleEventsPath,
2050
+ project: options.project
2051
+ });
2052
+ if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
2053
+ } catch (err) {
2054
+ console.error("staleness tick failed", err);
2055
+ }
2056
+ })();
2057
+ };
2058
+ const interval = setInterval(tick, intervalMs);
2059
+ if (typeof interval.unref === "function") interval.unref();
2060
+ return () => {
2061
+ stopped = true;
2062
+ clearInterval(interval);
2063
+ };
2064
+ }
2065
+ async function readErrorEvents(errorsPath) {
2066
+ try {
2067
+ const raw = await fs3.readFile(errorsPath, "utf8");
2068
+ const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2069
+ return dedupeIncidents(events);
2070
+ } catch (err) {
2071
+ if (err.code === "ENOENT") return [];
2072
+ throw err;
2073
+ }
2074
+ }
2075
+ function isSynthesizedHttpIncident(ev) {
2076
+ if (ev.exceptionType || ev.exceptionStacktrace) return false;
2077
+ if (ev.errorType) return false;
2078
+ if (!ev.attributes) return false;
2079
+ const synth = httpFailureMessageFromAttrs(ev.attributes);
2080
+ return synth !== void 0 && synth === ev.errorMessage;
2081
+ }
2082
+ function dedupeIncidents(events) {
2083
+ const seen = /* @__PURE__ */ new Set();
2084
+ const once = [];
2085
+ for (const ev of events) {
2086
+ const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
2087
+ if (key === void 0) {
2088
+ once.push(ev);
2089
+ continue;
2090
+ }
2091
+ if (seen.has(key)) continue;
2092
+ seen.add(key);
2093
+ once.push(ev);
2094
+ }
2095
+ const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
2096
+ const hasRealFailure = /* @__PURE__ */ new Set();
2097
+ for (const ev of once) {
2098
+ if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
2099
+ }
2100
+ return once.filter((ev) => {
2101
+ if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
2102
+ return !hasRealFailure.has(groupKey(ev));
2103
+ });
2104
+ }
2105
+ var SnapshotValidationError = class extends Error {
2106
+ constructor(issues) {
2107
+ super(`snapshot failed validation (${issues.length} invalid ${issues.length === 1 ? "entry" : "entries"})`);
2108
+ this.issues = issues;
2109
+ this.name = "SnapshotValidationError";
2110
+ }
2111
+ issues;
2112
+ };
2113
+ function describeZodIssues(error) {
2114
+ return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
2115
+ }
2116
+ function mergeSnapshot(graph, snapshot) {
2117
+ const exported = snapshot.graph;
2118
+ const incomingNodes = Array.isArray(exported.nodes) ? exported.nodes : [];
2119
+ const incomingEdges = Array.isArray(exported.edges) ? exported.edges : [];
2120
+ const issues = [];
2121
+ const validNodes = [];
2122
+ const validEdges = [];
2123
+ for (const node of incomingNodes) {
2124
+ if (node.attributes === void 0) continue;
2125
+ const parsed = GraphNodeSchema.safeParse(node.attributes);
2126
+ if (!parsed.success) {
2127
+ issues.push(`node "${node.key}": ${describeZodIssues(parsed.error)}`);
2128
+ continue;
2129
+ }
2130
+ validNodes.push({ key: node.key, attributes: parsed.data });
2131
+ }
2132
+ for (const edge of incomingEdges) {
2133
+ if (edge.attributes === void 0) continue;
2134
+ const parsed = GraphEdgeSchema.safeParse(edge.attributes);
2135
+ if (!parsed.success) {
2136
+ const label = edge.key ?? `${edge.source}->${edge.target}`;
2137
+ issues.push(`edge "${label}": ${describeZodIssues(parsed.error)}`);
2138
+ continue;
2139
+ }
2140
+ const id = edge.key ?? parsed.data.id;
2141
+ validEdges.push({ key: id, source: edge.source, target: edge.target, attributes: parsed.data });
2142
+ }
2143
+ if (issues.length > 0) {
2144
+ throw new SnapshotValidationError(issues);
2145
+ }
2146
+ let nodesAdded = 0;
2147
+ let edgesAdded = 0;
2148
+ for (const node of validNodes) {
2149
+ if (graph.hasNode(node.key)) continue;
2150
+ graph.addNode(node.key, node.attributes);
2151
+ nodesAdded++;
2152
+ }
2153
+ for (const edge of validEdges) {
2154
+ if (graph.hasEdge(edge.key)) continue;
2155
+ if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
2156
+ graph.addEdgeWithKey(edge.key, edge.source, edge.target, edge.attributes);
2157
+ edgesAdded++;
2158
+ }
2159
+ return { nodesAdded, edgesAdded };
2160
+ }
2161
+
2162
+ // src/traverse.ts
2163
+ import {
2164
+ BlastRadiusResultSchema,
2165
+ EdgeType as EdgeType3,
2166
+ NodeType as NodeType3,
2167
+ ObservedDependenciesResultSchema,
2168
+ PROV_RANK,
2169
+ Provenance as Provenance2,
2170
+ RootCauseResultSchema,
2171
+ TransitiveDependenciesResultSchema
2172
+ } from "@neat.is/types";
2173
+ var ROOT_CAUSE_MAX_DEPTH = 5;
2174
+ var BLAST_RADIUS_DEFAULT_DEPTH = 10;
2175
+ function isFrontierNode(graph, nodeId) {
2176
+ if (!graph.hasNode(nodeId)) return false;
2177
+ const attrs = graph.getNodeAttributes(nodeId);
2178
+ return attrs.type === NodeType3.FrontierNode;
2179
+ }
2180
+ function resolveOwningService(graph, nodeId) {
2181
+ if (!graph.hasNode(nodeId)) return null;
2182
+ const attrs = graph.getNodeAttributes(nodeId);
2183
+ if (attrs.type === NodeType3.ServiceNode) {
2184
+ return { id: nodeId, svc: attrs };
2185
+ }
2186
+ if (attrs.type === NodeType3.FileNode) {
2187
+ for (const edgeId of graph.inboundEdges(nodeId)) {
2188
+ const e = graph.getEdgeAttributes(edgeId);
2189
+ if (e.type !== EdgeType3.CONTAINS) continue;
2190
+ const owner = graph.getNodeAttributes(e.source);
2191
+ if (owner.type === NodeType3.ServiceNode) {
2192
+ return { id: e.source, svc: owner };
2193
+ }
2194
+ }
2195
+ }
2196
+ return null;
2197
+ }
2198
+ function bestEdgeBySource(graph, edgeIds) {
2199
+ const best = /* @__PURE__ */ new Map();
2200
+ for (const id of edgeIds) {
2201
+ const e = graph.getEdgeAttributes(id);
2202
+ if (isFrontierNode(graph, e.source)) continue;
2203
+ const cur = best.get(e.source);
2204
+ if (!cur || PROV_RANK[e.provenance] > PROV_RANK[cur.provenance]) {
2205
+ best.set(e.source, e);
2381
2206
  }
2382
2207
  }
2383
- if (span.statusCode !== 2) {
2384
- const status = httpResponseStatus(span);
2385
- if (span.exception) {
2386
- await recordExceptionIncident(ctx, span, ts);
2387
- } else if (status !== void 0 && status >= 500) {
2388
- await recordFailingResponseIncident(ctx, span, sourceId, ts, status, 1);
2389
- } else if (status !== void 0 && status >= 400 && spanMintsObservedEdge(span.kind)) {
2390
- await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status);
2208
+ return best;
2209
+ }
2210
+ function bestEdgeByTarget(graph, edgeIds) {
2211
+ const best = /* @__PURE__ */ new Map();
2212
+ for (const id of edgeIds) {
2213
+ const e = graph.getEdgeAttributes(id);
2214
+ if (isFrontierNode(graph, e.target)) continue;
2215
+ const cur = best.get(e.target);
2216
+ if (!cur || PROV_RANK[e.provenance] > PROV_RANK[cur.provenance]) {
2217
+ best.set(e.target, e);
2391
2218
  }
2392
2219
  }
2393
- void affectedNode;
2394
- if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
2220
+ return best;
2395
2221
  }
2396
- function promoteFrontierNodes(graph, opts = {}) {
2397
- const aliasIndex = /* @__PURE__ */ new Map();
2398
- graph.forEachNode((id, attrs) => {
2399
- const a = attrs;
2400
- if (a.type !== NodeType3.ServiceNode) return;
2401
- aliasIndex.set(a.name, id);
2402
- if (a.aliases) {
2403
- for (const alias of a.aliases) aliasIndex.set(alias, id);
2222
+ var PROVENANCE_CEILING = {
2223
+ OBSERVED: 1,
2224
+ INFERRED: 0.7,
2225
+ EXTRACTED: 0.5,
2226
+ STALE: 0.3
2227
+ };
2228
+ function volumeWeight(spanCount) {
2229
+ if (!spanCount || spanCount <= 0) return 0.5;
2230
+ const w = 0.5 + Math.log10(spanCount + 1) / 3;
2231
+ return Math.min(1, w);
2232
+ }
2233
+ function recencyWeight(ageMs) {
2234
+ if (ageMs === void 0) return 0.8;
2235
+ const hour = 60 * 60 * 1e3;
2236
+ if (ageMs <= hour) return 1;
2237
+ if (ageMs <= 24 * hour) {
2238
+ const t = (ageMs - hour) / (23 * hour);
2239
+ return 1 - 0.5 * t;
2240
+ }
2241
+ return 0.3;
2242
+ }
2243
+ function cleanlinessWeight(spanCount, errorCount) {
2244
+ if (!spanCount || spanCount <= 0) return 1;
2245
+ const rate = (errorCount ?? 0) / spanCount;
2246
+ if (rate <= 0.01) return 1;
2247
+ if (rate >= 0.5) return 0.3;
2248
+ return 1 - rate * 1.4;
2249
+ }
2250
+ function confidenceForEdge(edge, now = Date.now()) {
2251
+ const ceiling = PROVENANCE_CEILING[edge.provenance] ?? 0.5;
2252
+ const spanCount = edge.signal?.spanCount ?? edge.callCount;
2253
+ const ageMs = edge.signal?.lastObservedAgeMs ?? lastObservedAge(edge, now);
2254
+ if (spanCount === void 0 && ageMs === void 0 && edge.signal === void 0) {
2255
+ return ceiling;
2256
+ }
2257
+ const v = volumeWeight(spanCount);
2258
+ const r = recencyWeight(ageMs);
2259
+ const c = cleanlinessWeight(spanCount, edge.signal?.errorCount);
2260
+ return Math.max(0, Math.min(1, ceiling * v * r * c));
2261
+ }
2262
+ function lastObservedAge(edge, now) {
2263
+ if (!edge.lastObserved) return void 0;
2264
+ const t = Date.parse(edge.lastObserved);
2265
+ if (!Number.isFinite(t)) return void 0;
2266
+ return Math.max(0, now - t);
2267
+ }
2268
+ function confidenceFromMix(edges, now = Date.now()) {
2269
+ if (edges.length === 0) return 1;
2270
+ let product = 1;
2271
+ for (const e of edges) {
2272
+ product *= confidenceForEdge(e, now);
2273
+ }
2274
+ return Math.max(0, Math.min(1, product));
2275
+ }
2276
+ function longestIncomingWalk(graph, start, maxDepth) {
2277
+ let best = { path: [start], edges: [] };
2278
+ const visited = /* @__PURE__ */ new Set([start]);
2279
+ function step(node, path49, edges) {
2280
+ if (path49.length > best.path.length) {
2281
+ best = { path: [...path49], edges: [...edges] };
2404
2282
  }
2405
- });
2406
- const toPromote = [];
2407
- graph.forEachNode((id, attrs) => {
2408
- const a = attrs;
2409
- if (a.type !== NodeType3.FrontierNode) return;
2410
- const target = aliasIndex.get(a.host);
2411
- if (!target) return;
2412
- if (target === id) return;
2413
- toPromote.push({ frontierId: id, serviceId: target });
2414
- });
2415
- let promoted = 0;
2416
- for (const { frontierId: frontierId2, serviceId: serviceId5 } of toPromote) {
2417
- if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
2418
- const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
2419
- if (!gate.allowed) {
2420
- continue;
2421
- }
2283
+ if (path49.length - 1 >= maxDepth) return;
2284
+ const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
2285
+ for (const [srcId, edge] of incoming) {
2286
+ if (visited.has(srcId)) continue;
2287
+ visited.add(srcId);
2288
+ path49.push(srcId);
2289
+ edges.push(edge);
2290
+ step(srcId, path49, edges);
2291
+ path49.pop();
2292
+ edges.pop();
2293
+ visited.delete(srcId);
2422
2294
  }
2423
- rewireFrontierEdges(graph, frontierId2, serviceId5);
2424
- graph.dropNode(frontierId2);
2425
- promoted++;
2426
2295
  }
2427
- return promoted;
2296
+ step(start, [start], []);
2297
+ return best;
2428
2298
  }
2429
- function rewireFrontierEdges(graph, frontierId2, serviceId5) {
2430
- const inbound = [...graph.inboundEdges(frontierId2)];
2431
- const outbound = [...graph.outboundEdges(frontierId2)];
2432
- for (const edgeId of inbound) {
2433
- const edge = graph.getEdgeAttributes(edgeId);
2434
- rebuildEdge(graph, edge, edge.source, serviceId5, edgeId);
2299
+ function databaseRootCauseShape(graph, origin, walk4) {
2300
+ const targetDb = origin;
2301
+ const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
2302
+ if (candidatePairs.length === 0) return null;
2303
+ for (const id of walk4.path) {
2304
+ const owner = resolveOwningService(graph, id);
2305
+ if (!owner) continue;
2306
+ const { id: serviceId5, svc } = owner;
2307
+ const deps = svc.dependencies ?? {};
2308
+ for (const pair of candidatePairs) {
2309
+ const declared = deps[pair.driver];
2310
+ if (!declared) continue;
2311
+ const result = checkCompatibility(
2312
+ pair.driver,
2313
+ declared,
2314
+ targetDb.engine,
2315
+ targetDb.engineVersion
2316
+ );
2317
+ if (!result.compatible) {
2318
+ return {
2319
+ rootCauseNode: serviceId5,
2320
+ rootCauseReason: result.reason ?? "incompatible driver",
2321
+ ...result.minDriverVersion ? {
2322
+ fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
2323
+ } : {}
2324
+ };
2325
+ }
2326
+ }
2435
2327
  }
2436
- for (const edgeId of outbound) {
2437
- const edge = graph.getEdgeAttributes(edgeId);
2438
- rebuildEdge(graph, edge, serviceId5, edge.target, edgeId);
2328
+ return null;
2329
+ }
2330
+ function serviceRootCauseShape(graph, _origin, walk4) {
2331
+ for (const id of walk4.path) {
2332
+ const owner = resolveOwningService(graph, id);
2333
+ if (!owner) continue;
2334
+ const { id: serviceId5, svc } = owner;
2335
+ const deps = svc.dependencies ?? {};
2336
+ const serviceNodeEngine = svc.nodeEngine;
2337
+ for (const constraint of nodeEngineConstraints()) {
2338
+ const declared = deps[constraint.package];
2339
+ if (!declared) continue;
2340
+ const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
2341
+ if (!result.compatible && result.reason) {
2342
+ return {
2343
+ rootCauseNode: serviceId5,
2344
+ rootCauseReason: result.reason,
2345
+ ...result.requiredNodeVersion ? {
2346
+ fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
2347
+ } : {}
2348
+ };
2349
+ }
2350
+ }
2351
+ for (const conflict of packageConflicts()) {
2352
+ const declared = deps[conflict.package];
2353
+ if (!declared) continue;
2354
+ const requiredDeclared = deps[conflict.requires.name];
2355
+ const result = checkPackageConflict(conflict, declared, requiredDeclared);
2356
+ if (!result.compatible && result.reason) {
2357
+ return {
2358
+ rootCauseNode: serviceId5,
2359
+ rootCauseReason: result.reason,
2360
+ fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
2361
+ };
2362
+ }
2363
+ }
2439
2364
  }
2365
+ return null;
2440
2366
  }
2441
- function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
2442
- graph.dropEdge(oldEdgeId);
2443
- const newId = edge.provenance === Provenance2.OBSERVED ? observedEdgeId(newSource, newTarget, edge.type) : edge.provenance === Provenance2.INFERRED ? inferredEdgeId(newSource, newTarget, edge.type) : extractedEdgeId(newSource, newTarget, edge.type);
2444
- if (graph.hasEdge(newId)) {
2445
- const existing = graph.getEdgeAttributes(newId);
2446
- const merged = {
2447
- ...existing,
2448
- callCount: (existing.callCount ?? 0) + (edge.callCount ?? 0),
2449
- lastObserved: pickLater(existing.lastObserved, edge.lastObserved)
2450
- };
2451
- graph.replaceEdgeAttributes(newId, merged);
2452
- return;
2367
+ function fileRootCauseShape(graph, origin, walk4) {
2368
+ const owner = resolveOwningService(graph, origin.id);
2369
+ if (!owner) return null;
2370
+ return serviceRootCauseShape(graph, owner.svc, walk4);
2371
+ }
2372
+ var rootCauseShapes = {
2373
+ [NodeType3.DatabaseNode]: databaseRootCauseShape,
2374
+ [NodeType3.ServiceNode]: serviceRootCauseShape,
2375
+ [NodeType3.FileNode]: fileRootCauseShape
2376
+ };
2377
+ function getRootCause(graph, errorNodeId, errorEvent, incidents) {
2378
+ if (!graph.hasNode(errorNodeId)) return null;
2379
+ const origin = graph.getNodeAttributes(errorNodeId);
2380
+ const shape = rootCauseShapes[origin.type];
2381
+ if (shape) {
2382
+ const walk4 = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
2383
+ const match = shape(graph, origin, walk4);
2384
+ if (match) {
2385
+ const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
2386
+ return RootCauseResultSchema.parse({
2387
+ rootCauseNode: match.rootCauseNode,
2388
+ rootCauseReason: reason,
2389
+ traversalPath: walk4.path,
2390
+ edgeProvenances: walk4.edges.map((e) => e.provenance),
2391
+ confidence: confidenceFromMix(walk4.edges),
2392
+ fixRecommendation: match.fixRecommendation
2393
+ });
2394
+ }
2453
2395
  }
2454
- const rebuilt = {
2455
- ...edge,
2456
- id: newId,
2457
- source: newSource,
2458
- target: newTarget
2459
- };
2460
- graph.addEdgeWithKey(newId, newSource, newTarget, rebuilt);
2396
+ if (origin.type === NodeType3.ServiceNode) {
2397
+ const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
2398
+ if (crossService) return crossService;
2399
+ }
2400
+ return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
2461
2401
  }
2462
- function pickLater(a, b) {
2463
- if (!a) return b;
2464
- if (!b) return a;
2465
- return new Date(a).getTime() >= new Date(b).getTime() ? a : b;
2402
+ var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
2403
+ function incidentMatchesNode(ev, nodeId) {
2404
+ return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
2466
2405
  }
2467
- function makeSpanHandler(ctx) {
2468
- return (span) => handleSpan(ctx, span);
2406
+ function localizeFromIncidents(nodeId, incidents, errorEvent) {
2407
+ const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
2408
+ const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
2409
+ if (relevant.length === 0) return null;
2410
+ const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
2411
+ const attrs = latest.attributes ?? {};
2412
+ const filepath = codeFilepathOf(attrs);
2413
+ const lineno = codeLinenoOf(attrs);
2414
+ const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
2415
+ const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
2416
+ const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
2417
+ const count = sameMode.length;
2418
+ const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
2419
+ const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
2420
+ if (location) reasonParts.push(`surfaced at ${location}`);
2421
+ const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
2422
+ const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
2423
+ const fileNode = localizesToFile ? latest.affectedNode : void 0;
2424
+ const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
2425
+ return {
2426
+ rootCauseNode: fileNode ?? nodeId,
2427
+ rootCauseReason,
2428
+ ...fileNode ? { fileNode } : {},
2429
+ ...fixRecommendation ? { fixRecommendation } : {}
2430
+ };
2469
2431
  }
2470
- async function markStaleEdges(graph, options = {}) {
2471
- const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
2472
- const now = options.now ?? Date.now();
2473
- const events = [];
2474
- const project = options.project ?? DEFAULT_PROJECT;
2475
- graph.forEachEdge((id, attrs) => {
2476
- const e = attrs;
2477
- if (e.provenance !== Provenance2.OBSERVED) return;
2478
- if (!e.lastObserved) return;
2479
- const threshold = thresholdForEdgeType(e.type, thresholds);
2480
- const age = now - new Date(e.lastObserved).getTime();
2481
- if (age > threshold) {
2482
- const updated = { ...e, provenance: Provenance2.STALE, confidence: 0.3 };
2483
- graph.replaceEdgeAttributes(id, updated);
2484
- events.push({
2485
- edgeId: id,
2486
- source: e.source,
2487
- target: e.target,
2488
- edgeType: e.type,
2489
- thresholdMs: threshold,
2490
- ageMs: age,
2491
- lastObserved: e.lastObserved,
2492
- transitionedAt: new Date(now).toISOString()
2493
- });
2494
- emitNeatEvent({
2495
- type: "stale-transition",
2496
- project,
2497
- payload: {
2498
- edgeId: id,
2499
- from: Provenance2.OBSERVED,
2500
- to: Provenance2.STALE
2501
- }
2502
- });
2503
- }
2432
+ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
2433
+ const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
2434
+ if (!loc) return null;
2435
+ const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
2436
+ const edgeProvenances = loc.fileNode ? [Provenance2.OBSERVED] : [];
2437
+ return RootCauseResultSchema.parse({
2438
+ rootCauseNode: loc.rootCauseNode,
2439
+ rootCauseReason: loc.rootCauseReason,
2440
+ traversalPath,
2441
+ edgeProvenances,
2442
+ confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
2443
+ ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
2504
2444
  });
2505
- if (options.staleEventsPath && events.length > 0) {
2506
- await appendStaleEvents(options.staleEventsPath, events);
2507
- }
2508
- return { count: events.length, events };
2509
2445
  }
2510
- async function appendStaleEvents(staleEventsPath, events) {
2511
- await fs3.mkdir(path3.dirname(staleEventsPath), { recursive: true });
2512
- const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
2513
- await fs3.appendFile(staleEventsPath, lines, "utf8");
2446
+ function isFailingCallEdge(e) {
2447
+ return e.type === EdgeType3.CALLS && (e.signal?.errorCount ?? 0) > 0;
2514
2448
  }
2515
- async function readStaleEvents(staleEventsPath) {
2516
- try {
2517
- const raw = await fs3.readFile(staleEventsPath, "utf8");
2518
- return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2519
- } catch (err) {
2520
- if (err.code === "ENOENT") return [];
2521
- throw err;
2449
+ function callSourcesForService(graph, serviceId5) {
2450
+ const ids = [serviceId5];
2451
+ for (const edgeId of graph.outboundEdges(serviceId5)) {
2452
+ const e = graph.getEdgeAttributes(edgeId);
2453
+ if (e.type !== EdgeType3.CONTAINS) continue;
2454
+ const tgt = graph.getNodeAttributes(e.target);
2455
+ if (tgt.type === NodeType3.FileNode) ids.push(e.target);
2522
2456
  }
2457
+ return ids;
2523
2458
  }
2524
- function startStalenessLoop(graph, options = {}) {
2525
- let stopped = false;
2526
- const intervalMs = options.intervalMs ?? 6e4;
2527
- const tick = () => {
2528
- if (stopped) return;
2529
- void (async () => {
2530
- try {
2531
- await markStaleEdges(graph, {
2532
- thresholds: options.thresholds,
2533
- staleEventsPath: options.staleEventsPath,
2534
- project: options.project
2535
- });
2536
- if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
2537
- } catch (err) {
2538
- console.error("staleness tick failed", err);
2459
+ function failingCallDominates(e, id, curEdge, curId) {
2460
+ const ec = e.signal?.errorCount ?? 0;
2461
+ const cc = curEdge.signal?.errorCount ?? 0;
2462
+ if (ec !== cc) return ec > cc;
2463
+ if (PROV_RANK[e.provenance] !== PROV_RANK[curEdge.provenance]) {
2464
+ return PROV_RANK[e.provenance] > PROV_RANK[curEdge.provenance];
2465
+ }
2466
+ return id < curId;
2467
+ }
2468
+ function dominantFailingCall(graph, serviceId5, visited) {
2469
+ let best = null;
2470
+ for (const src of callSourcesForService(graph, serviceId5)) {
2471
+ for (const edgeId of graph.outboundEdges(src)) {
2472
+ const e = graph.getEdgeAttributes(edgeId);
2473
+ if (!isFailingCallEdge(e)) continue;
2474
+ if (isFrontierNode(graph, e.target)) continue;
2475
+ const owner = resolveOwningService(graph, e.target);
2476
+ if (!owner || visited.has(owner.id)) continue;
2477
+ if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
2478
+ best = { nextService: owner.id, edge: e };
2539
2479
  }
2540
- })();
2541
- };
2542
- const interval = setInterval(tick, intervalMs);
2543
- if (typeof interval.unref === "function") interval.unref();
2544
- return () => {
2545
- stopped = true;
2546
- clearInterval(interval);
2547
- };
2480
+ }
2481
+ }
2482
+ return best;
2548
2483
  }
2549
- async function readErrorEvents(errorsPath) {
2550
- try {
2551
- const raw = await fs3.readFile(errorsPath, "utf8");
2552
- const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
2553
- return dedupeIncidents(events);
2554
- } catch (err) {
2555
- if (err.code === "ENOENT") return [];
2556
- throw err;
2484
+ function followFailingCallChain(graph, originServiceId, maxDepth) {
2485
+ const path49 = [originServiceId];
2486
+ const edges = [];
2487
+ const visited = /* @__PURE__ */ new Set([originServiceId]);
2488
+ let current = originServiceId;
2489
+ for (let depth = 0; depth < maxDepth; depth++) {
2490
+ const hop = dominantFailingCall(graph, current, visited);
2491
+ if (!hop) break;
2492
+ path49.push(hop.nextService);
2493
+ edges.push(hop.edge);
2494
+ visited.add(hop.nextService);
2495
+ current = hop.nextService;
2557
2496
  }
2497
+ if (edges.length === 0) return null;
2498
+ return { path: path49, edges, culprit: current };
2558
2499
  }
2559
- function isSynthesizedHttpIncident(ev) {
2560
- if (ev.exceptionType || ev.exceptionStacktrace) return false;
2561
- if (ev.errorType) return false;
2562
- if (!ev.attributes) return false;
2563
- const synth = httpFailureMessageFromAttrs(ev.attributes);
2564
- return synth !== void 0 && synth === ev.errorMessage;
2500
+ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
2501
+ const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
2502
+ if (!chain) return null;
2503
+ const culprit = chain.culprit;
2504
+ const path49 = [...chain.path];
2505
+ const edgeProvenances = chain.edges.map((e) => e.provenance);
2506
+ const baseConfidence = confidenceFromMix(chain.edges);
2507
+ const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
2508
+ const loc = localizeFromIncidents(culprit, incidents, errorEvent);
2509
+ if (loc) {
2510
+ let rootCauseNode = culprit;
2511
+ if (loc.fileNode) {
2512
+ path49.push(loc.fileNode);
2513
+ edgeProvenances.push(Provenance2.OBSERVED);
2514
+ rootCauseNode = loc.fileNode;
2515
+ }
2516
+ return RootCauseResultSchema.parse({
2517
+ rootCauseNode,
2518
+ rootCauseReason: loc.rootCauseReason,
2519
+ traversalPath: path49,
2520
+ edgeProvenances,
2521
+ confidence,
2522
+ ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
2523
+ });
2524
+ }
2525
+ const lastEdge = chain.edges[chain.edges.length - 1];
2526
+ const errs = lastEdge.signal?.errorCount ?? 0;
2527
+ const culpritName = culprit.replace(/^service:/, "");
2528
+ return RootCauseResultSchema.parse({
2529
+ rootCauseNode: culprit,
2530
+ rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
2531
+ traversalPath: path49,
2532
+ edgeProvenances,
2533
+ confidence,
2534
+ fixRecommendation: `Inspect ${culpritName}'s failing handler`
2535
+ });
2565
2536
  }
2566
- function dedupeIncidents(events) {
2567
- const seen = /* @__PURE__ */ new Set();
2568
- const once = [];
2569
- for (const ev of events) {
2570
- const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
2571
- if (key === void 0) {
2572
- once.push(ev);
2573
- continue;
2537
+ function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
2538
+ if (!graph.hasNode(nodeId)) {
2539
+ return BlastRadiusResultSchema.parse({ origin: nodeId, affectedNodes: [], totalAffected: 0 });
2540
+ }
2541
+ const seen = /* @__PURE__ */ new Map();
2542
+ const queue = [{ nodeId, distance: 0, path: [nodeId], pathEdges: [] }];
2543
+ const enqueued = /* @__PURE__ */ new Set([nodeId]);
2544
+ while (queue.length > 0) {
2545
+ const frame = queue.shift();
2546
+ if (frame.distance > 0 && frame.pathEdges.length > 0) {
2547
+ const lastEdge = frame.pathEdges[frame.pathEdges.length - 1];
2548
+ seen.set(frame.nodeId, {
2549
+ nodeId: frame.nodeId,
2550
+ distance: frame.distance,
2551
+ edgeProvenance: lastEdge.provenance,
2552
+ path: frame.path,
2553
+ confidence: confidenceFromMix(frame.pathEdges)
2554
+ });
2574
2555
  }
2575
- if (seen.has(key)) continue;
2576
- seen.add(key);
2577
- once.push(ev);
2556
+ if (frame.distance >= maxDepth) continue;
2557
+ const incoming = bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId));
2558
+ for (const [srcId, edge] of incoming) {
2559
+ if (enqueued.has(srcId)) continue;
2560
+ enqueued.add(srcId);
2561
+ queue.push({
2562
+ nodeId: srcId,
2563
+ distance: frame.distance + 1,
2564
+ path: [...frame.path, srcId],
2565
+ pathEdges: [...frame.pathEdges, edge]
2566
+ });
2567
+ }
2568
+ }
2569
+ const affectedNodes = [...seen.values()].sort(
2570
+ (a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
2571
+ );
2572
+ return BlastRadiusResultSchema.parse({
2573
+ origin: nodeId,
2574
+ affectedNodes,
2575
+ totalAffected: affectedNodes.length
2576
+ });
2577
+ }
2578
+ var TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH = 3;
2579
+ var TRANSITIVE_DEPENDENCIES_MAX_DEPTH = 10;
2580
+ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH) {
2581
+ if (!graph.hasNode(nodeId)) {
2582
+ return TransitiveDependenciesResultSchema.parse({
2583
+ origin: nodeId,
2584
+ depth,
2585
+ dependencies: [],
2586
+ total: 0
2587
+ });
2578
2588
  }
2579
- const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
2580
- const hasRealFailure = /* @__PURE__ */ new Set();
2581
- for (const ev of once) {
2582
- if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
2589
+ const seen = /* @__PURE__ */ new Map();
2590
+ const queue = [{ nodeId, distance: 0, edge: null }];
2591
+ const enqueued = /* @__PURE__ */ new Set([nodeId]);
2592
+ while (queue.length > 0) {
2593
+ const frame = queue.shift();
2594
+ if (frame.distance > 0 && frame.edge && frame.edge.type !== EdgeType3.CONTAINS) {
2595
+ seen.set(frame.nodeId, {
2596
+ nodeId: frame.nodeId,
2597
+ distance: frame.distance,
2598
+ edgeType: frame.edge.type,
2599
+ provenance: frame.edge.provenance
2600
+ });
2601
+ }
2602
+ if (frame.distance >= depth) continue;
2603
+ const outgoing = bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
2604
+ for (const [tgtId, edge] of outgoing) {
2605
+ if (enqueued.has(tgtId)) continue;
2606
+ enqueued.add(tgtId);
2607
+ queue.push({ nodeId: tgtId, distance: frame.distance + 1, edge });
2608
+ }
2583
2609
  }
2584
- return once.filter((ev) => {
2585
- if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
2586
- return !hasRealFailure.has(groupKey(ev));
2610
+ const dependencies = [...seen.values()].sort(
2611
+ (a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
2612
+ );
2613
+ return TransitiveDependenciesResultSchema.parse({
2614
+ origin: nodeId,
2615
+ depth,
2616
+ dependencies,
2617
+ total: dependencies.length
2587
2618
  });
2588
2619
  }
2589
- var SnapshotValidationError = class extends Error {
2590
- constructor(issues) {
2591
- super(`snapshot failed validation (${issues.length} invalid ${issues.length === 1 ? "entry" : "entries"})`);
2592
- this.issues = issues;
2593
- this.name = "SnapshotValidationError";
2620
+ function getObservedDependencies(graph, nodeId) {
2621
+ if (!graph.hasNode(nodeId)) {
2622
+ return ObservedDependenciesResultSchema.parse({
2623
+ origin: nodeId,
2624
+ dependencies: [],
2625
+ observed: false,
2626
+ inboundObservedCount: 0,
2627
+ hasExtractedOutbound: false
2628
+ });
2594
2629
  }
2595
- issues;
2596
- };
2597
- function describeZodIssues(error) {
2598
- return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
2599
- }
2600
- function mergeSnapshot(graph, snapshot) {
2601
- const exported = snapshot.graph;
2602
- const incomingNodes = Array.isArray(exported.nodes) ? exported.nodes : [];
2603
- const incomingEdges = Array.isArray(exported.edges) ? exported.edges : [];
2604
- const issues = [];
2605
- const validNodes = [];
2606
- const validEdges = [];
2607
- for (const node of incomingNodes) {
2608
- if (node.attributes === void 0) continue;
2609
- const parsed = GraphNodeSchema.safeParse(node.attributes);
2610
- if (!parsed.success) {
2611
- issues.push(`node "${node.key}": ${describeZodIssues(parsed.error)}`);
2612
- continue;
2630
+ const attrs = graph.getNodeAttributes(nodeId);
2631
+ const scope = [nodeId];
2632
+ if (attrs.type === NodeType3.ServiceNode) {
2633
+ for (const edgeId of graph.outboundEdges(nodeId)) {
2634
+ const e = graph.getEdgeAttributes(edgeId);
2635
+ if (e.type !== EdgeType3.CONTAINS) continue;
2636
+ const owned = graph.getNodeAttributes(e.target);
2637
+ if (owned.type === NodeType3.FileNode) scope.push(e.target);
2613
2638
  }
2614
- validNodes.push({ key: node.key, attributes: parsed.data });
2615
2639
  }
2616
- for (const edge of incomingEdges) {
2617
- if (edge.attributes === void 0) continue;
2618
- const parsed = GraphEdgeSchema.safeParse(edge.attributes);
2619
- if (!parsed.success) {
2620
- const label = edge.key ?? `${edge.source}->${edge.target}`;
2621
- issues.push(`edge "${label}": ${describeZodIssues(parsed.error)}`);
2622
- continue;
2640
+ const dependencies = [];
2641
+ const seenEdge = /* @__PURE__ */ new Set();
2642
+ let hasExtractedOutbound = false;
2643
+ for (const src of scope) {
2644
+ for (const edgeId of graph.outboundEdges(src)) {
2645
+ const e = graph.getEdgeAttributes(edgeId);
2646
+ if (e.type === EdgeType3.CONTAINS) continue;
2647
+ if (e.provenance === Provenance2.OBSERVED) {
2648
+ if (!seenEdge.has(e.id)) {
2649
+ seenEdge.add(e.id);
2650
+ dependencies.push(e);
2651
+ }
2652
+ } else if (e.provenance === Provenance2.EXTRACTED) {
2653
+ hasExtractedOutbound = true;
2654
+ }
2623
2655
  }
2624
- const id = edge.key ?? parsed.data.id;
2625
- validEdges.push({ key: id, source: edge.source, target: edge.target, attributes: parsed.data });
2626
- }
2627
- if (issues.length > 0) {
2628
- throw new SnapshotValidationError(issues);
2629
- }
2630
- let nodesAdded = 0;
2631
- let edgesAdded = 0;
2632
- for (const node of validNodes) {
2633
- if (graph.hasNode(node.key)) continue;
2634
- graph.addNode(node.key, node.attributes);
2635
- nodesAdded++;
2636
2656
  }
2637
- for (const edge of validEdges) {
2638
- if (graph.hasEdge(edge.key)) continue;
2639
- if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
2640
- graph.addEdgeWithKey(edge.key, edge.source, edge.target, edge.attributes);
2641
- edgesAdded++;
2657
+ let inboundObservedCount = 0;
2658
+ for (const tgt of scope) {
2659
+ for (const edgeId of graph.inboundEdges(tgt)) {
2660
+ const e = graph.getEdgeAttributes(edgeId);
2661
+ if (e.type === EdgeType3.CONTAINS) continue;
2662
+ if (e.provenance === Provenance2.OBSERVED) inboundObservedCount += 1;
2663
+ }
2642
2664
  }
2643
- return { nodesAdded, edgesAdded };
2665
+ dependencies.sort(
2666
+ (a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
2667
+ );
2668
+ return ObservedDependenciesResultSchema.parse({
2669
+ origin: nodeId,
2670
+ dependencies,
2671
+ observed: dependencies.length > 0 || inboundObservedCount > 0,
2672
+ inboundObservedCount,
2673
+ hasExtractedOutbound
2674
+ });
2644
2675
  }
2645
2676
 
2646
2677
  // src/extract/errors.ts
@@ -3417,14 +3448,14 @@ function buildServiceHostIndex(services) {
3417
3448
  }
3418
3449
  async function walkSourceFiles(dir) {
3419
3450
  const out = [];
3420
- async function walk3(current) {
3451
+ async function walk4(current) {
3421
3452
  const entries = await fs10.readdir(current, { withFileTypes: true }).catch(() => []);
3422
3453
  for (const entry of entries) {
3423
3454
  const full = path10.join(current, entry.name);
3424
3455
  if (entry.isDirectory()) {
3425
3456
  if (IGNORED_DIRS.has(entry.name)) continue;
3426
3457
  if (await isPythonVenvDir(full)) continue;
3427
- await walk3(full);
3458
+ await walk4(full);
3428
3459
  } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
3429
3460
  // would attribute our instrumentation imports to the user's service.
3430
3461
  !isNeatAuthoredSourceFile(entry.name)) {
@@ -3432,7 +3463,7 @@ async function walkSourceFiles(dir) {
3432
3463
  }
3433
3464
  }
3434
3465
  }
3435
- await walk3(dir);
3466
+ await walk4(dir);
3436
3467
  return out;
3437
3468
  }
3438
3469
  async function loadSourceFiles(dir) {
@@ -4588,20 +4619,20 @@ import {
4588
4619
  } from "@neat.is/types";
4589
4620
  async function walkConfigFiles(dir) {
4590
4621
  const out = [];
4591
- async function walk3(current) {
4622
+ async function walk4(current) {
4592
4623
  const entries = await fs14.readdir(current, { withFileTypes: true });
4593
4624
  for (const entry of entries) {
4594
4625
  const full = path21.join(current, entry.name);
4595
4626
  if (entry.isDirectory()) {
4596
4627
  if (IGNORED_DIRS.has(entry.name)) continue;
4597
4628
  if (await isPythonVenvDir(full)) continue;
4598
- await walk3(full);
4629
+ await walk4(full);
4599
4630
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
4600
4631
  out.push(full);
4601
4632
  }
4602
4633
  }
4603
4634
  }
4604
- await walk3(dir);
4635
+ await walk4(dir);
4605
4636
  return out;
4606
4637
  }
4607
4638
  async function addConfigNodes(graph, services, scanPath) {
@@ -4653,6 +4684,7 @@ async function addConfigNodes(graph, services, scanPath) {
4653
4684
  import path22 from "path";
4654
4685
  import Parser2 from "tree-sitter";
4655
4686
  import JavaScript2 from "tree-sitter-javascript";
4687
+ import Python2 from "tree-sitter-python";
4656
4688
  import {
4657
4689
  EdgeType as EdgeType8,
4658
4690
  NodeType as NodeType9,
@@ -4672,6 +4704,11 @@ function makeJsParser2() {
4672
4704
  p.setLanguage(JavaScript2);
4673
4705
  return p;
4674
4706
  }
4707
+ function makePyParser2() {
4708
+ const p = new Parser2();
4709
+ p.setLanguage(Python2);
4710
+ return p;
4711
+ }
4675
4712
  var ROUTER_METHODS = /* @__PURE__ */ new Set([
4676
4713
  "get",
4677
4714
  "post",
@@ -4684,6 +4721,7 @@ var ROUTER_METHODS = /* @__PURE__ */ new Set([
4684
4721
  ]);
4685
4722
  var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
4686
4723
  var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
4724
+ var FASTAPI_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
4687
4725
  function canonicalizeTemplate(raw) {
4688
4726
  let p = raw.split("?")[0].split("#")[0];
4689
4727
  if (!p.startsWith("/")) p = "/" + p;
@@ -4911,8 +4949,102 @@ function nextRoutesFromFile(source, relFile, parser) {
4911
4949
  }
4912
4950
  return [];
4913
4951
  }
4952
+ function pyStaticStringText(node) {
4953
+ if (node.type !== "string") return null;
4954
+ for (let i = 0; i < node.namedChildCount; i++) {
4955
+ const child = node.namedChild(i);
4956
+ if (child?.type === "interpolation") return null;
4957
+ if (child?.type === "string_content") return child.text;
4958
+ }
4959
+ return "";
4960
+ }
4961
+ function keywordArrayStrings(argsNode, key) {
4962
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
4963
+ const arg = argsNode.namedChild(i);
4964
+ if (arg?.type !== "keyword_argument") continue;
4965
+ if (arg.childForFieldName("name")?.text !== key) continue;
4966
+ const val = arg.childForFieldName("value");
4967
+ if (!val || val.type !== "list") return [];
4968
+ const out = [];
4969
+ for (let j = 0; j < val.namedChildCount; j++) {
4970
+ const el = val.namedChild(j);
4971
+ if (el?.type === "string") {
4972
+ const s = pyStaticStringText(el);
4973
+ if (s) out.push(s);
4974
+ }
4975
+ }
4976
+ return out;
4977
+ }
4978
+ return [];
4979
+ }
4980
+ function collectApiRouterPrefixes(root) {
4981
+ const prefixes = /* @__PURE__ */ new Map();
4982
+ walk(root, (node) => {
4983
+ if (node.type !== "assignment") return;
4984
+ const right = node.childForFieldName("right");
4985
+ if (!right || right.type !== "call") return;
4986
+ const fn = right.childForFieldName("function");
4987
+ if (!fn) return;
4988
+ const ctor = fn.type === "attribute" ? fn.childForFieldName("attribute")?.text : fn.text;
4989
+ if (ctor !== "APIRouter") return;
4990
+ const left = node.childForFieldName("left");
4991
+ if (!left || left.type !== "identifier") return;
4992
+ const args = right.childForFieldName("arguments");
4993
+ if (!args) return;
4994
+ for (let i = 0; i < args.namedChildCount; i++) {
4995
+ const arg = args.namedChild(i);
4996
+ if (arg?.type !== "keyword_argument") continue;
4997
+ if (arg.childForFieldName("name")?.text !== "prefix") continue;
4998
+ const val = arg.childForFieldName("value");
4999
+ const p = val ? pyStaticStringText(val) : null;
5000
+ if (p !== null) prefixes.set(left.text, p);
5001
+ }
5002
+ });
5003
+ return prefixes;
5004
+ }
5005
+ function fastapiRoutesFromSource(source, parser) {
5006
+ const tree = parseSource2(parser, source);
5007
+ const prefixes = collectApiRouterPrefixes(tree.rootNode);
5008
+ const out = [];
5009
+ walk(tree.rootNode, (node) => {
5010
+ if (node.type !== "decorator") return;
5011
+ const call = node.namedChild(0);
5012
+ if (!call || call.type !== "call") return;
5013
+ const fn = call.childForFieldName("function");
5014
+ if (!fn || fn.type !== "attribute") return;
5015
+ const method = fn.childForFieldName("attribute")?.text?.toLowerCase();
5016
+ if (!method) return;
5017
+ const isVerb = FASTAPI_METHODS.has(method);
5018
+ if (!isVerb && method !== "api_route") return;
5019
+ const args = call.childForFieldName("arguments");
5020
+ const first = args?.namedChild(0);
5021
+ if (!first || first.type !== "string") return;
5022
+ const rawPath = pyStaticStringText(first);
5023
+ if (rawPath === null || !rawPath.startsWith("/")) return;
5024
+ const obj = fn.childForFieldName("object")?.text;
5025
+ const prefix = obj ? prefixes.get(obj) ?? "" : "";
5026
+ const pathTemplate = canonicalizeTemplate(prefix + rawPath);
5027
+ const line = node.startPosition.row + 1;
5028
+ if (isVerb) {
5029
+ out.push({ method: method.toUpperCase(), pathTemplate, line, framework: "fastapi" });
5030
+ return;
5031
+ }
5032
+ const methods = keywordArrayStrings(args, "methods");
5033
+ const list = methods.length > 0 ? methods : ["ALL"];
5034
+ for (const m of list) {
5035
+ out.push({
5036
+ method: m === "ALL" ? "ALL" : m.toUpperCase(),
5037
+ pathTemplate,
5038
+ line,
5039
+ framework: "fastapi"
5040
+ });
5041
+ }
5042
+ });
5043
+ return out;
5044
+ }
4914
5045
  async function addRoutes(graph, services) {
4915
5046
  const jsParser = makeJsParser2();
5047
+ const pyParser = makePyParser2();
4916
5048
  let nodesAdded = 0;
4917
5049
  let edgesAdded = 0;
4918
5050
  for (const service of services) {
@@ -4924,15 +5056,20 @@ async function addRoutes(graph, services) {
4924
5056
  const hasFastify = deps["fastify"] !== void 0;
4925
5057
  const hasHono = deps["hono"] !== void 0;
4926
5058
  const hasNext = deps["next"] !== void 0;
4927
- if (!hasExpress && !hasFastify && !hasHono && !hasNext) continue;
5059
+ const hasFastapi = deps["fastapi"] !== void 0;
5060
+ if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasFastapi) continue;
4928
5061
  const files = await loadSourceFiles(service.dir);
4929
5062
  for (const file of files) {
4930
5063
  if (isTestPath(file.path)) continue;
4931
- if (!JS_ROUTE_EXTENSIONS.has(path22.extname(file.path))) continue;
5064
+ const ext = path22.extname(file.path);
5065
+ const isPy = ext === ".py";
5066
+ if (!JS_ROUTE_EXTENSIONS.has(ext) && !isPy) continue;
4932
5067
  const relFile = toPosix2(path22.relative(service.dir, file.path));
4933
5068
  let routes;
4934
5069
  try {
4935
- if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
5070
+ if (isPy) {
5071
+ routes = hasFastapi ? fastapiRoutesFromSource(file.content, pyParser) : [];
5072
+ } else if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
4936
5073
  routes = nextRoutesFromFile(file.content, relFile, jsParser);
4937
5074
  } else if (hasExpress || hasFastify || hasHono) {
4938
5075
  routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify, hasHono);
@@ -5034,20 +5171,20 @@ function grpcMethodsFromProto(content, fqPackage) {
5034
5171
  }
5035
5172
  async function walkProtoFiles(dir) {
5036
5173
  const out = [];
5037
- async function walk3(current) {
5174
+ async function walk4(current) {
5038
5175
  const entries = await fs15.readdir(current, { withFileTypes: true }).catch(() => []);
5039
5176
  for (const entry of entries) {
5040
5177
  const full = path23.join(current, entry.name);
5041
5178
  if (entry.isDirectory()) {
5042
5179
  if (IGNORED_DIRS.has(entry.name)) continue;
5043
5180
  if (await isPythonVenvDir(full)) continue;
5044
- await walk3(full);
5181
+ await walk4(full);
5045
5182
  } else if (entry.isFile() && path23.extname(entry.name) === PROTO_EXTENSION) {
5046
5183
  out.push(full);
5047
5184
  }
5048
5185
  }
5049
5186
  }
5050
- await walk3(dir);
5187
+ await walk4(dir);
5051
5188
  return out;
5052
5189
  }
5053
5190
  async function addGrpcMethods(graph, services) {
@@ -5126,7 +5263,7 @@ import {
5126
5263
  import path24 from "path";
5127
5264
  import Parser3 from "tree-sitter";
5128
5265
  import JavaScript3 from "tree-sitter-javascript";
5129
- import Python2 from "tree-sitter-python";
5266
+ import Python3 from "tree-sitter-python";
5130
5267
  import {
5131
5268
  EdgeType as EdgeType10,
5132
5269
  Provenance as Provenance9,
@@ -5186,14 +5323,14 @@ function makeJsParser3() {
5186
5323
  p.setLanguage(JavaScript3);
5187
5324
  return p;
5188
5325
  }
5189
- function makePyParser2() {
5326
+ function makePyParser3() {
5190
5327
  const p = new Parser3();
5191
- p.setLanguage(Python2);
5328
+ p.setLanguage(Python3);
5192
5329
  return p;
5193
5330
  }
5194
5331
  async function addHttpCallEdges(graph, services) {
5195
5332
  const jsParser = makeJsParser3();
5196
- const pyParser = makePyParser2();
5333
+ const pyParser = makePyParser3();
5197
5334
  const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
5198
5335
  let nodesAdded = 0;
5199
5336
  let edgesAdded = 0;
@@ -6133,6 +6270,120 @@ async function mongooseCrossFileEndpoints(files, serviceDir) {
6133
6270
  return out;
6134
6271
  }
6135
6272
 
6273
+ // src/extract/calls/sqlalchemy.ts
6274
+ import path32 from "path";
6275
+ import Parser5 from "tree-sitter";
6276
+ import Python4 from "tree-sitter-python";
6277
+ import { infraId as infraId8 } from "@neat.is/types";
6278
+ var SQLALCHEMY_IMPORT_RE = /(?:from|import)\s+(?:flask_sqlalchemy|sqlalchemy)\b/;
6279
+ var PARSE_CHUNK5 = 16384;
6280
+ function makePyParser4() {
6281
+ const p = new Parser5();
6282
+ p.setLanguage(Python4);
6283
+ return p;
6284
+ }
6285
+ function parseSource5(parser, source) {
6286
+ return parser.parse(
6287
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK5)
6288
+ );
6289
+ }
6290
+ function flaskSqlalchemyTableName(className) {
6291
+ return className.replace(/((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))/g, "_$1").toLowerCase().replace(/^_+/, "");
6292
+ }
6293
+ function namedChildren(node) {
6294
+ const out = [];
6295
+ for (let i = 0; i < node.namedChildCount; i++) {
6296
+ const c = node.namedChild(i);
6297
+ if (c) out.push(c);
6298
+ }
6299
+ return out;
6300
+ }
6301
+ function pyStaticStringText2(node) {
6302
+ if (node.type !== "string") return null;
6303
+ for (const child of namedChildren(node)) {
6304
+ if (child.type === "interpolation") return null;
6305
+ if (child.type === "string_content") return child.text;
6306
+ }
6307
+ return "";
6308
+ }
6309
+ function explicitTablename(body) {
6310
+ for (const stmt of namedChildren(body)) {
6311
+ if (stmt.type !== "expression_statement") continue;
6312
+ const assign = stmt.namedChild(0);
6313
+ if (assign?.type !== "assignment") continue;
6314
+ if (assign.childForFieldName("left")?.text !== "__tablename__") continue;
6315
+ const right = assign.childForFieldName("right");
6316
+ if (right?.type === "string") {
6317
+ const s = pyStaticStringText2(right);
6318
+ return s ? { name: s } : "computed";
6319
+ }
6320
+ return "computed";
6321
+ }
6322
+ return null;
6323
+ }
6324
+ function extendsFlaskModel(cls) {
6325
+ const supers = cls.childForFieldName("superclasses");
6326
+ if (!supers) return false;
6327
+ for (const a of namedChildren(supers)) {
6328
+ const t = a.text;
6329
+ if (t === "db.Model" || t === "Model" || t.endsWith(".Model")) return true;
6330
+ }
6331
+ return false;
6332
+ }
6333
+ function walk3(node, visit) {
6334
+ visit(node);
6335
+ for (const c of namedChildren(node)) walk3(c, visit);
6336
+ }
6337
+ function sqlalchemyEndpointsFromFile(file, serviceDir) {
6338
+ if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
6339
+ const tree = parseSource5(makePyParser4(), file.content);
6340
+ const out = [];
6341
+ const seen = /* @__PURE__ */ new Set();
6342
+ const push = (name, line) => {
6343
+ if (seen.has(name)) return;
6344
+ seen.add(name);
6345
+ out.push({
6346
+ infraId: infraId8("sql-table", name),
6347
+ name,
6348
+ kind: "sql-table",
6349
+ edgeType: "CALLS",
6350
+ confidenceKind: "verified-call-site",
6351
+ evidence: {
6352
+ file: path32.relative(serviceDir, file.path),
6353
+ line,
6354
+ snippet: snippet(file.content, line)
6355
+ }
6356
+ });
6357
+ };
6358
+ walk3(tree.rootNode, (node) => {
6359
+ if (node.type === "class_definition") {
6360
+ const body = node.childForFieldName("body");
6361
+ const nameNode = node.childForFieldName("name");
6362
+ if (!body || !nameNode) return;
6363
+ const line = node.startPosition.row + 1;
6364
+ const explicit = explicitTablename(body);
6365
+ if (explicit === "computed") return;
6366
+ if (explicit) {
6367
+ push(explicit.name, line);
6368
+ return;
6369
+ }
6370
+ if (extendsFlaskModel(node)) push(flaskSqlalchemyTableName(nameNode.text), line);
6371
+ return;
6372
+ }
6373
+ if (node.type === "call") {
6374
+ const fn = node.childForFieldName("function");
6375
+ const fnText = fn?.text;
6376
+ if (fnText !== "Table" && fnText !== "sa.Table" && fnText !== "sqlalchemy.Table") return;
6377
+ const first = node.childForFieldName("arguments")?.namedChild(0);
6378
+ if (first?.type === "string") {
6379
+ const s = pyStaticStringText2(first);
6380
+ if (s) push(s, node.startPosition.row + 1);
6381
+ }
6382
+ }
6383
+ });
6384
+ return out;
6385
+ }
6386
+
6136
6387
  // src/extract/calls/index.ts
6137
6388
  function edgeTypeFromEndpoint(ep) {
6138
6389
  switch (ep.edgeType) {
@@ -6165,6 +6416,7 @@ async function addExternalEndpointEdges(graph, services) {
6165
6416
  endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
6166
6417
  endpoints.push(...supabaseEndpointsFromFile(maskedFile, service.dir));
6167
6418
  endpoints.push(...mongooseEndpointsFromFile(maskedFile, service.dir));
6419
+ endpoints.push(...sqlalchemyEndpointsFromFile(maskedFile, service.dir));
6168
6420
  }
6169
6421
  endpoints.push(...await mongooseCrossFileEndpoints(maskedFiles, service.dir));
6170
6422
  if (endpoints.length === 0) continue;
@@ -6237,14 +6489,14 @@ async function addCallEdges(graph, services) {
6237
6489
  }
6238
6490
 
6239
6491
  // src/extract/infra/docker-compose.ts
6240
- import path32 from "path";
6492
+ import path33 from "path";
6241
6493
  import { EdgeType as EdgeType13, Provenance as Provenance13, confidenceForExtracted as confidenceForExtracted11 } from "@neat.is/types";
6242
6494
 
6243
6495
  // src/extract/infra/shared.ts
6244
- import { NodeType as NodeType13, Provenance as Provenance12, confidenceForExtracted as confidenceForExtracted10, infraId as infraId8 } from "@neat.is/types";
6496
+ import { NodeType as NodeType13, Provenance as Provenance12, confidenceForExtracted as confidenceForExtracted10, infraId as infraId9 } from "@neat.is/types";
6245
6497
  function makeInfraNode(kind, name, provider = "self", extras) {
6246
6498
  return {
6247
- id: infraId8(kind, name),
6499
+ id: infraId9(kind, name),
6248
6500
  type: NodeType13.InfraNode,
6249
6501
  name,
6250
6502
  provider,
@@ -6307,7 +6559,7 @@ function dependsOnList(value) {
6307
6559
  }
6308
6560
  function serviceNameToServiceNode(name, services) {
6309
6561
  for (const s of services) {
6310
- if (s.node.name === name || path32.basename(s.dir) === name) return s.node.id;
6562
+ if (s.node.name === name || path33.basename(s.dir) === name) return s.node.id;
6311
6563
  }
6312
6564
  return null;
6313
6565
  }
@@ -6316,7 +6568,7 @@ async function addComposeInfra(graph, scanPath, services) {
6316
6568
  let edgesAdded = 0;
6317
6569
  let composePath = null;
6318
6570
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
6319
- const abs = path32.join(scanPath, name);
6571
+ const abs = path33.join(scanPath, name);
6320
6572
  if (await exists(abs)) {
6321
6573
  composePath = abs;
6322
6574
  break;
@@ -6329,13 +6581,13 @@ async function addComposeInfra(graph, scanPath, services) {
6329
6581
  } catch (err) {
6330
6582
  recordExtractionError(
6331
6583
  "infra docker-compose",
6332
- path32.relative(scanPath, composePath),
6584
+ path33.relative(scanPath, composePath),
6333
6585
  err
6334
6586
  );
6335
6587
  return { nodesAdded, edgesAdded };
6336
6588
  }
6337
6589
  if (!compose?.services) return { nodesAdded, edgesAdded };
6338
- const evidenceFile = path32.relative(scanPath, composePath).split(path32.sep).join("/");
6590
+ const evidenceFile = path33.relative(scanPath, composePath).split(path33.sep).join("/");
6339
6591
  const composeNameToNodeId = /* @__PURE__ */ new Map();
6340
6592
  for (const [composeName, svc] of Object.entries(compose.services)) {
6341
6593
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -6376,7 +6628,7 @@ async function addComposeInfra(graph, scanPath, services) {
6376
6628
  }
6377
6629
 
6378
6630
  // src/extract/infra/dockerfile.ts
6379
- import path33 from "path";
6631
+ import path34 from "path";
6380
6632
  import { promises as fs16 } from "fs";
6381
6633
  import { EdgeType as EdgeType14, Provenance as Provenance14, confidenceForExtracted as confidenceForExtracted12 } from "@neat.is/types";
6382
6634
  function readDockerfile(content) {
@@ -6407,7 +6659,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6407
6659
  let nodesAdded = 0;
6408
6660
  let edgesAdded = 0;
6409
6661
  for (const service of services) {
6410
- const dockerfilePath = path33.join(service.dir, "Dockerfile");
6662
+ const dockerfilePath = path34.join(service.dir, "Dockerfile");
6411
6663
  if (!await exists(dockerfilePath)) continue;
6412
6664
  let content;
6413
6665
  try {
@@ -6415,7 +6667,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6415
6667
  } catch (err) {
6416
6668
  recordExtractionError(
6417
6669
  "infra dockerfile",
6418
- path33.relative(scanPath, dockerfilePath),
6670
+ path34.relative(scanPath, dockerfilePath),
6419
6671
  err
6420
6672
  );
6421
6673
  continue;
@@ -6427,8 +6679,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6427
6679
  graph.addNode(node.id, node);
6428
6680
  nodesAdded++;
6429
6681
  }
6430
- const relDockerfile = toPosix2(path33.relative(service.dir, dockerfilePath));
6431
- const evidenceFile = toPosix2(path33.relative(scanPath, dockerfilePath));
6682
+ const relDockerfile = toPosix2(path34.relative(service.dir, dockerfilePath));
6683
+ const evidenceFile = toPosix2(path34.relative(scanPath, dockerfilePath));
6432
6684
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
6433
6685
  graph,
6434
6686
  service.pkg.name,
@@ -6480,7 +6732,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6480
6732
 
6481
6733
  // src/extract/infra/terraform.ts
6482
6734
  import { promises as fs17 } from "fs";
6483
- import path34 from "path";
6735
+ import path35 from "path";
6484
6736
  import { EdgeType as EdgeType15, Provenance as Provenance15, confidenceForExtracted as confidenceForExtracted13 } from "@neat.is/types";
6485
6737
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
6486
6738
  var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
@@ -6491,11 +6743,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
6491
6743
  for (const entry of entries) {
6492
6744
  if (entry.isDirectory()) {
6493
6745
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
6494
- const child = path34.join(start, entry.name);
6746
+ const child = path35.join(start, entry.name);
6495
6747
  if (await isPythonVenvDir(child)) continue;
6496
6748
  out.push(...await walkTfFiles(child, depth + 1, max));
6497
6749
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
6498
- out.push(path34.join(start, entry.name));
6750
+ out.push(path35.join(start, entry.name));
6499
6751
  }
6500
6752
  }
6501
6753
  return out;
@@ -6527,7 +6779,7 @@ async function addTerraformResources(graph, scanPath) {
6527
6779
  const files = await walkTfFiles(scanPath);
6528
6780
  for (const file of files) {
6529
6781
  const content = await fs17.readFile(file, "utf8");
6530
- const evidenceFile = toPosix2(path34.relative(scanPath, file));
6782
+ const evidenceFile = toPosix2(path35.relative(scanPath, file));
6531
6783
  const resources = [];
6532
6784
  const byKey = /* @__PURE__ */ new Map();
6533
6785
  RESOURCE_RE.lastIndex = 0;
@@ -6584,7 +6836,7 @@ async function addTerraformResources(graph, scanPath) {
6584
6836
 
6585
6837
  // src/extract/infra/k8s.ts
6586
6838
  import { promises as fs18 } from "fs";
6587
- import path35 from "path";
6839
+ import path36 from "path";
6588
6840
  import { parseAllDocuments as parseAllDocuments2 } from "yaml";
6589
6841
  var K8S_KIND_TO_INFRA_KIND = {
6590
6842
  Service: "k8s-service",
@@ -6602,11 +6854,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
6602
6854
  for (const entry of entries) {
6603
6855
  if (entry.isDirectory()) {
6604
6856
  if (IGNORED_DIRS.has(entry.name)) continue;
6605
- const child = path35.join(start, entry.name);
6857
+ const child = path36.join(start, entry.name);
6606
6858
  if (await isPythonVenvDir(child)) continue;
6607
6859
  out.push(...await walkYamlFiles2(child, depth + 1, max));
6608
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path35.extname(entry.name))) {
6609
- out.push(path35.join(start, entry.name));
6860
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path36.extname(entry.name))) {
6861
+ out.push(path36.join(start, entry.name));
6610
6862
  }
6611
6863
  }
6612
6864
  return out;
@@ -6639,13 +6891,13 @@ async function addK8sResources(graph, scanPath) {
6639
6891
 
6640
6892
  // src/extract/infra/cloudflare.ts
6641
6893
  import { promises as fs19 } from "fs";
6642
- import path36 from "path";
6894
+ import path37 from "path";
6643
6895
  import { parse as parseToml2 } from "smol-toml";
6644
6896
  import { EdgeType as EdgeType16, Provenance as Provenance16, confidenceForExtracted as confidenceForExtracted14 } from "@neat.is/types";
6645
6897
  var WRANGLER_FILENAMES = ["wrangler.toml", "wrangler.jsonc", "wrangler.json"];
6646
6898
  async function readWranglerConfig(dir) {
6647
6899
  for (const filename of WRANGLER_FILENAMES) {
6648
- const abs = path36.join(dir, filename);
6900
+ const abs = path37.join(dir, filename);
6649
6901
  if (!await exists(abs)) continue;
6650
6902
  const raw = await fs19.readFile(abs, "utf8");
6651
6903
  const config = filename === "wrangler.toml" ? parseToml2(raw) : JSON.parse(maskCommentsInSource(raw));
@@ -6708,11 +6960,11 @@ async function addCloudflareWorkers(graph, services, scanPath) {
6708
6960
  try {
6709
6961
  read = await readWranglerConfig(service.dir);
6710
6962
  } catch (err) {
6711
- recordExtractionError("infra cloudflare", path36.relative(scanPath, service.dir), err);
6963
+ recordExtractionError("infra cloudflare", path37.relative(scanPath, service.dir), err);
6712
6964
  continue;
6713
6965
  }
6714
6966
  if (!read || !read.config.name) continue;
6715
- const evidenceFile = toPosix2(path36.relative(scanPath, path36.join(service.dir, read.relFile)));
6967
+ const evidenceFile = toPosix2(path37.relative(scanPath, path37.join(service.dir, read.relFile)));
6716
6968
  discovered.push({ service, config: read.config, relFile: read.relFile, raw: read.raw, evidenceFile });
6717
6969
  }
6718
6970
  for (const worker of discovered) {
@@ -6724,7 +6976,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
6724
6976
  }
6725
6977
  let anchorId = service.node.id;
6726
6978
  if (config.main) {
6727
- const entryRelPath = toPosix2(path36.normalize(config.main));
6979
+ const entryRelPath = toPosix2(path37.normalize(config.main));
6728
6980
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
6729
6981
  graph,
6730
6982
  service.pkg.name,
@@ -6871,12 +7123,12 @@ async function addCloudflareWorkers(graph, services, scanPath) {
6871
7123
 
6872
7124
  // src/extract/infra/vercel.ts
6873
7125
  import { promises as fs20 } from "fs";
6874
- import path37 from "path";
7126
+ import path38 from "path";
6875
7127
  import { EdgeType as EdgeType17 } from "@neat.is/types";
6876
7128
  var VERCEL_CONFIG_FILENAMES = ["vercel.json", "vercel.jsonc"];
6877
7129
  async function readVercelConfig(dir) {
6878
7130
  for (const filename of VERCEL_CONFIG_FILENAMES) {
6879
- const abs = path37.join(dir, filename);
7131
+ const abs = path38.join(dir, filename);
6880
7132
  if (!await exists(abs)) continue;
6881
7133
  const raw = await fs20.readFile(abs, "utf8");
6882
7134
  const config = JSON.parse(maskCommentsInSource(raw));
@@ -6885,7 +7137,7 @@ async function readVercelConfig(dir) {
6885
7137
  return null;
6886
7138
  }
6887
7139
  async function readLinkedProjectName(dir) {
6888
- const abs = path37.join(dir, ".vercel", "project.json");
7140
+ const abs = path38.join(dir, ".vercel", "project.json");
6889
7141
  if (!await exists(abs)) return void 0;
6890
7142
  const parsed = JSON.parse(await fs20.readFile(abs, "utf8"));
6891
7143
  return typeof parsed.projectName === "string" ? parsed.projectName : void 0;
@@ -6903,7 +7155,7 @@ async function addVercelServices(graph, services, scanPath) {
6903
7155
  read = await readVercelConfig(service.dir);
6904
7156
  projectName = await readLinkedProjectName(service.dir);
6905
7157
  } catch (err) {
6906
- recordExtractionError("infra vercel", path37.relative(scanPath, service.dir), err);
7158
+ recordExtractionError("infra vercel", path38.relative(scanPath, service.dir), err);
6907
7159
  continue;
6908
7160
  }
6909
7161
  if (!read && !projectName) continue;
@@ -6919,7 +7171,7 @@ async function addVercelServices(graph, services, scanPath) {
6919
7171
  const anchorId = service.node.id;
6920
7172
  if (!read) continue;
6921
7173
  const { config, relFile, raw } = read;
6922
- const evidenceFile = toPosix2(path37.relative(scanPath, path37.join(service.dir, relFile)));
7174
+ const evidenceFile = toPosix2(path38.relative(scanPath, path38.join(service.dir, relFile)));
6923
7175
  const add = (edgeType, kind, name) => {
6924
7176
  if (!name) return;
6925
7177
  const result = emitPlatformResourceEdge(
@@ -6948,13 +7200,13 @@ async function addVercelServices(graph, services, scanPath) {
6948
7200
 
6949
7201
  // src/extract/infra/railway.ts
6950
7202
  import { promises as fs21 } from "fs";
6951
- import path38 from "path";
7203
+ import path39 from "path";
6952
7204
  import { parse as parseToml3 } from "smol-toml";
6953
7205
  import { EdgeType as EdgeType18 } from "@neat.is/types";
6954
7206
  var RAILWAY_FILENAMES = ["railway.toml", "railway.json", "railway.jsonc"];
6955
7207
  async function readRailwayConfig(dir) {
6956
7208
  for (const filename of RAILWAY_FILENAMES) {
6957
- const abs = path38.join(dir, filename);
7209
+ const abs = path39.join(dir, filename);
6958
7210
  if (!await exists(abs)) continue;
6959
7211
  const raw = await fs21.readFile(abs, "utf8");
6960
7212
  const config = filename === "railway.toml" ? parseToml3(raw) : JSON.parse(maskCommentsInSource(raw));
@@ -6970,7 +7222,7 @@ async function addRailwayServices(graph, services, scanPath) {
6970
7222
  try {
6971
7223
  read = await readRailwayConfig(service.dir);
6972
7224
  } catch (err) {
6973
- recordExtractionError("infra railway", path38.relative(scanPath, service.dir), err);
7225
+ recordExtractionError("infra railway", path39.relative(scanPath, service.dir), err);
6974
7226
  continue;
6975
7227
  }
6976
7228
  if (!read) continue;
@@ -6980,7 +7232,7 @@ async function addRailwayServices(graph, services, scanPath) {
6980
7232
  }
6981
7233
  const anchorId = service.node.id;
6982
7234
  const { config, relFile, raw } = read;
6983
- const evidenceFile = toPosix2(path38.relative(scanPath, path38.join(service.dir, relFile)));
7235
+ const evidenceFile = toPosix2(path39.relative(scanPath, path39.join(service.dir, relFile)));
6984
7236
  const add = (edgeType, kind, name) => {
6985
7237
  if (!name) return;
6986
7238
  const result = emitPlatformResourceEdge(
@@ -7005,12 +7257,12 @@ async function addRailwayServices(graph, services, scanPath) {
7005
7257
 
7006
7258
  // src/extract/infra/supabase.ts
7007
7259
  import { promises as fs22 } from "fs";
7008
- import path39 from "path";
7260
+ import path40 from "path";
7009
7261
  import { parse as parseToml4 } from "smol-toml";
7010
7262
  import { EdgeType as EdgeType19 } from "@neat.is/types";
7011
7263
  async function readSupabaseConfig(dir) {
7012
- const relFile = path39.join("supabase", "config.toml");
7013
- const abs = path39.join(dir, relFile);
7264
+ const relFile = path40.join("supabase", "config.toml");
7265
+ const abs = path40.join(dir, relFile);
7014
7266
  if (!await exists(abs)) return null;
7015
7267
  const raw = await fs22.readFile(abs, "utf8");
7016
7268
  const config = parseToml4(raw);
@@ -7024,7 +7276,7 @@ async function addSupabaseProjects(graph, services, scanPath) {
7024
7276
  try {
7025
7277
  read = await readSupabaseConfig(service.dir);
7026
7278
  } catch (err) {
7027
- recordExtractionError("infra supabase", path39.relative(scanPath, service.dir), err);
7279
+ recordExtractionError("infra supabase", path40.relative(scanPath, service.dir), err);
7028
7280
  continue;
7029
7281
  }
7030
7282
  if (!read) continue;
@@ -7039,7 +7291,7 @@ async function addSupabaseProjects(graph, services, scanPath) {
7039
7291
  });
7040
7292
  }
7041
7293
  const anchorId = service.node.id;
7042
- const evidenceFile = toPosix2(path39.relative(scanPath, path39.join(service.dir, relFile)));
7294
+ const evidenceFile = toPosix2(path40.relative(scanPath, path40.join(service.dir, relFile)));
7043
7295
  const add = (edgeType, kind, name) => {
7044
7296
  if (!name) return;
7045
7297
  const result = emitPlatformResourceEdge(
@@ -7080,11 +7332,11 @@ async function addInfra(graph, scanPath, services) {
7080
7332
  }
7081
7333
 
7082
7334
  // src/extract/index.ts
7083
- import path41 from "path";
7335
+ import path42 from "path";
7084
7336
 
7085
7337
  // src/extract/retire.ts
7086
7338
  import { existsSync as existsSync2 } from "fs";
7087
- import path40 from "path";
7339
+ import path41 from "path";
7088
7340
  import { NodeType as NodeType14, Provenance as Provenance17 } from "@neat.is/types";
7089
7341
  function dropOrphanedFileNodes(graph) {
7090
7342
  const orphans = [];
@@ -7118,11 +7370,11 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
7118
7370
  if (edge.provenance !== Provenance17.EXTRACTED) return;
7119
7371
  const evidenceFile = edge.evidence?.file;
7120
7372
  if (!evidenceFile) return;
7121
- if (path40.isAbsolute(evidenceFile)) {
7373
+ if (path41.isAbsolute(evidenceFile)) {
7122
7374
  if (!existsSync2(evidenceFile)) toDrop.push(id);
7123
7375
  return;
7124
7376
  }
7125
- const found = bases.some((base) => existsSync2(path40.join(base, evidenceFile)));
7377
+ const found = bases.some((base) => existsSync2(path41.join(base, evidenceFile)));
7126
7378
  if (!found) toDrop.push(id);
7127
7379
  });
7128
7380
  for (const id of toDrop) graph.dropEdge(id);
@@ -7164,7 +7416,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
7164
7416
  }
7165
7417
  const droppedEntries = drainDroppedExtracted();
7166
7418
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
7167
- const rejectedPath = path41.join(path41.dirname(opts.errorsPath), "rejected.ndjson");
7419
+ const rejectedPath = path42.join(path42.dirname(opts.errorsPath), "rejected.ndjson");
7168
7420
  try {
7169
7421
  await writeRejectedExtracted(droppedEntries, rejectedPath);
7170
7422
  } catch (err) {
@@ -7482,7 +7734,7 @@ function computeDivergences(graph, opts = {}) {
7482
7734
 
7483
7735
  // src/persist.ts
7484
7736
  import { promises as fs23 } from "fs";
7485
- import path42 from "path";
7737
+ import path43 from "path";
7486
7738
  import { Provenance as Provenance19, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
7487
7739
  var SCHEMA_VERSION = 4;
7488
7740
  function migrateV1ToV2(payload) {
@@ -7519,7 +7771,7 @@ function migrateV2ToV3(payload) {
7519
7771
  return { ...payload, schemaVersion: 3 };
7520
7772
  }
7521
7773
  async function ensureDir(filePath) {
7522
- await fs23.mkdir(path42.dirname(filePath), { recursive: true });
7774
+ await fs23.mkdir(path43.dirname(filePath), { recursive: true });
7523
7775
  }
7524
7776
  async function saveGraphToDisk(graph, outPath) {
7525
7777
  await ensureDir(outPath);
@@ -7675,23 +7927,23 @@ function canonicalJson(value) {
7675
7927
  }
7676
7928
 
7677
7929
  // src/projects.ts
7678
- import path43 from "path";
7930
+ import path44 from "path";
7679
7931
  function pathsForProject(project, baseDir) {
7680
7932
  if (project === DEFAULT_PROJECT) {
7681
7933
  return {
7682
- snapshotPath: path43.join(baseDir, "graph.json"),
7683
- errorsPath: path43.join(baseDir, "errors.ndjson"),
7684
- staleEventsPath: path43.join(baseDir, "stale-events.ndjson"),
7685
- embeddingsCachePath: path43.join(baseDir, "embeddings.json"),
7686
- policyViolationsPath: path43.join(baseDir, "policy-violations.ndjson")
7934
+ snapshotPath: path44.join(baseDir, "graph.json"),
7935
+ errorsPath: path44.join(baseDir, "errors.ndjson"),
7936
+ staleEventsPath: path44.join(baseDir, "stale-events.ndjson"),
7937
+ embeddingsCachePath: path44.join(baseDir, "embeddings.json"),
7938
+ policyViolationsPath: path44.join(baseDir, "policy-violations.ndjson")
7687
7939
  };
7688
7940
  }
7689
7941
  return {
7690
- snapshotPath: path43.join(baseDir, `${project}.json`),
7691
- errorsPath: path43.join(baseDir, `errors.${project}.ndjson`),
7692
- staleEventsPath: path43.join(baseDir, `stale-events.${project}.ndjson`),
7693
- embeddingsCachePath: path43.join(baseDir, `embeddings.${project}.json`),
7694
- policyViolationsPath: path43.join(baseDir, `policy-violations.${project}.ndjson`)
7942
+ snapshotPath: path44.join(baseDir, `${project}.json`),
7943
+ errorsPath: path44.join(baseDir, `errors.${project}.ndjson`),
7944
+ staleEventsPath: path44.join(baseDir, `stale-events.${project}.ndjson`),
7945
+ embeddingsCachePath: path44.join(baseDir, `embeddings.${project}.json`),
7946
+ policyViolationsPath: path44.join(baseDir, `policy-violations.${project}.ndjson`)
7695
7947
  };
7696
7948
  }
7697
7949
  var Projects = class {
@@ -7732,7 +7984,7 @@ function parseExtraProjects(raw) {
7732
7984
  // src/registry.ts
7733
7985
  import { promises as fs25 } from "fs";
7734
7986
  import os2 from "os";
7735
- import path44 from "path";
7987
+ import path45 from "path";
7736
7988
  import {
7737
7989
  RegistryFileSchema
7738
7990
  } from "@neat.is/types";
@@ -7740,20 +7992,20 @@ var LOCK_TIMEOUT_MS = 5e3;
7740
7992
  var LOCK_RETRY_MS = 50;
7741
7993
  function neatHome() {
7742
7994
  const override = process.env.NEAT_HOME;
7743
- if (override && override.length > 0) return path44.resolve(override);
7744
- return path44.join(os2.homedir(), ".neat");
7995
+ if (override && override.length > 0) return path45.resolve(override);
7996
+ return path45.join(os2.homedir(), ".neat");
7745
7997
  }
7746
7998
  function registryPath() {
7747
- return path44.join(neatHome(), "projects.json");
7999
+ return path45.join(neatHome(), "projects.json");
7748
8000
  }
7749
8001
  function registryLockPath() {
7750
- return path44.join(neatHome(), "projects.json.lock");
8002
+ return path45.join(neatHome(), "projects.json.lock");
7751
8003
  }
7752
8004
  function daemonPidPath() {
7753
- return path44.join(neatHome(), "neatd.pid");
8005
+ return path45.join(neatHome(), "neatd.pid");
7754
8006
  }
7755
8007
  function daemonsDir() {
7756
- return path44.join(neatHome(), "daemons");
8008
+ return path45.join(neatHome(), "daemons");
7757
8009
  }
7758
8010
  function isFiniteInt(v) {
7759
8011
  return typeof v === "number" && Number.isFinite(v);
@@ -7794,7 +8046,7 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
7794
8046
  const out = [];
7795
8047
  for (const name of names) {
7796
8048
  if (!name.endsWith(".json")) continue;
7797
- const file = path44.join(dir, name);
8049
+ const file = path45.join(dir, name);
7798
8050
  let raw;
7799
8051
  try {
7800
8052
  raw = await fs25.readFile(file, "utf8");
@@ -7915,7 +8167,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
7915
8167
  }
7916
8168
  }
7917
8169
  async function normalizeProjectPath(input) {
7918
- const resolved = path44.resolve(input);
8170
+ const resolved = path45.resolve(input);
7919
8171
  try {
7920
8172
  return await fs25.realpath(resolved);
7921
8173
  } catch {
@@ -7923,7 +8175,7 @@ async function normalizeProjectPath(input) {
7923
8175
  }
7924
8176
  }
7925
8177
  async function writeAtomically(target, contents) {
7926
- await fs25.mkdir(path44.dirname(target), { recursive: true });
8178
+ await fs25.mkdir(path45.dirname(target), { recursive: true });
7927
8179
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
7928
8180
  const fd = await fs25.open(tmp, "w");
7929
8181
  try {
@@ -7936,7 +8188,7 @@ async function writeAtomically(target, contents) {
7936
8188
  }
7937
8189
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
7938
8190
  const deadline = Date.now() + timeoutMs;
7939
- await fs25.mkdir(path44.dirname(lockPath), { recursive: true });
8191
+ await fs25.mkdir(path45.dirname(lockPath), { recursive: true });
7940
8192
  let probedHolder = false;
7941
8193
  while (true) {
7942
8194
  try {
@@ -8132,13 +8384,13 @@ import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } f
8132
8384
 
8133
8385
  // src/extend/index.ts
8134
8386
  import { promises as fs27 } from "fs";
8135
- import path46 from "path";
8387
+ import path47 from "path";
8136
8388
  import os3 from "os";
8137
8389
  import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
8138
8390
 
8139
8391
  // src/installers/package-manager.ts
8140
8392
  import { promises as fs26 } from "fs";
8141
- import path45 from "path";
8393
+ import path46 from "path";
8142
8394
  import { spawn } from "child_process";
8143
8395
  var LOCKFILE_PRIORITY = [
8144
8396
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -8160,22 +8412,22 @@ async function exists2(p) {
8160
8412
  }
8161
8413
  }
8162
8414
  async function detectPackageManager(serviceDir) {
8163
- let dir = path45.resolve(serviceDir);
8415
+ let dir = path46.resolve(serviceDir);
8164
8416
  const stops = /* @__PURE__ */ new Set();
8165
8417
  for (let i = 0; i < 64; i++) {
8166
8418
  if (stops.has(dir)) break;
8167
8419
  stops.add(dir);
8168
8420
  for (const candidate of LOCKFILE_PRIORITY) {
8169
- const lockPath = path45.join(dir, candidate.lockfile);
8421
+ const lockPath = path46.join(dir, candidate.lockfile);
8170
8422
  if (await exists2(lockPath)) {
8171
8423
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
8172
8424
  }
8173
8425
  }
8174
- const parent = path45.dirname(dir);
8426
+ const parent = path46.dirname(dir);
8175
8427
  if (parent === dir) break;
8176
8428
  dir = parent;
8177
8429
  }
8178
- return { pm: "npm", cwd: path45.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
8430
+ return { pm: "npm", cwd: path46.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
8179
8431
  }
8180
8432
  async function runPackageManagerInstall(cmd) {
8181
8433
  return new Promise((resolve) => {
@@ -8224,7 +8476,7 @@ async function fileExists2(p) {
8224
8476
  }
8225
8477
  }
8226
8478
  async function readPackageJson(scanPath) {
8227
- const pkgPath = path46.join(scanPath, "package.json");
8479
+ const pkgPath = path47.join(scanPath, "package.json");
8228
8480
  const raw = await fs27.readFile(pkgPath, "utf8");
8229
8481
  return JSON.parse(raw);
8230
8482
  }
@@ -8238,27 +8490,27 @@ var HOOK_WALK_SKIP_DIRS = /* @__PURE__ */ new Set([
8238
8490
  ]);
8239
8491
  async function findHookFiles(scanPath) {
8240
8492
  const found = [];
8241
- const walk3 = async (dir) => {
8493
+ const walk4 = async (dir) => {
8242
8494
  const entries = await fs27.readdir(dir, { withFileTypes: true }).catch(() => []);
8243
8495
  for (const entry of entries) {
8244
8496
  if (entry.isDirectory()) {
8245
8497
  if (entry.name.startsWith(".") || HOOK_WALK_SKIP_DIRS.has(entry.name)) continue;
8246
- await walk3(path46.join(dir, entry.name));
8498
+ await walk4(path47.join(dir, entry.name));
8247
8499
  } else if (entry.isFile()) {
8248
8500
  if ((entry.name.startsWith("instrumentation") || entry.name.startsWith("otel-init")) && /\.(ts|js|cjs|mjs)$/.test(entry.name)) {
8249
- const rel = path46.relative(scanPath, path46.join(dir, entry.name));
8250
- found.push(rel.split(path46.sep).join("/"));
8501
+ const rel = path47.relative(scanPath, path47.join(dir, entry.name));
8502
+ found.push(rel.split(path47.sep).join("/"));
8251
8503
  }
8252
8504
  }
8253
8505
  }
8254
8506
  };
8255
- await walk3(scanPath);
8507
+ await walk4(scanPath);
8256
8508
  return found.sort();
8257
8509
  }
8258
8510
  async function pickPrimaryHookFile(scanPath, hookFiles, snippet2) {
8259
8511
  let fallback = null;
8260
8512
  for (const file of hookFiles) {
8261
- const content = await fs27.readFile(path46.join(scanPath, file), "utf8");
8513
+ const content = await fs27.readFile(path47.join(scanPath, file), "utf8");
8262
8514
  const patched = splicedContent(content, snippet2);
8263
8515
  if (patched !== null) return { file, content, patched };
8264
8516
  if (fallback === null) fallback = { file, content };
@@ -8266,11 +8518,11 @@ async function pickPrimaryHookFile(scanPath, hookFiles, snippet2) {
8266
8518
  return { file: fallback.file, content: fallback.content, patched: null };
8267
8519
  }
8268
8520
  function extendLogPath() {
8269
- return process.env.NEAT_EXTEND_LOG ?? path46.join(os3.homedir(), ".neat", "extend-log.ndjson");
8521
+ return process.env.NEAT_EXTEND_LOG ?? path47.join(os3.homedir(), ".neat", "extend-log.ndjson");
8270
8522
  }
8271
8523
  async function appendExtendLog(entry) {
8272
8524
  const logPath = extendLogPath();
8273
- await fs27.mkdir(path46.dirname(logPath), { recursive: true });
8525
+ await fs27.mkdir(path47.dirname(logPath), { recursive: true });
8274
8526
  await fs27.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
8275
8527
  }
8276
8528
  function splicedContent(fileContent, snippet2) {
@@ -8329,7 +8581,7 @@ function lookupInstrumentation(library, installedVersion) {
8329
8581
  }
8330
8582
  async function describeProjectInstrumentation(ctx) {
8331
8583
  const hookFiles = await findHookFiles(ctx.scanPath);
8332
- const envNeat = await fileExists2(path46.join(ctx.scanPath, ".env.neat"));
8584
+ const envNeat = await fileExists2(path47.join(ctx.scanPath, ".env.neat"));
8333
8585
  const registryInstrPackages = new Set(
8334
8586
  registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
8335
8587
  );
@@ -8351,7 +8603,7 @@ async function applyExtension(ctx, args, options) {
8351
8603
  );
8352
8604
  }
8353
8605
  for (const file of hookFiles) {
8354
- const content = await fs27.readFile(path46.join(ctx.scanPath, file), "utf8");
8606
+ const content = await fs27.readFile(path47.join(ctx.scanPath, file), "utf8");
8355
8607
  if (content.includes(args.registration_snippet)) {
8356
8608
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
8357
8609
  }
@@ -8363,10 +8615,10 @@ async function applyExtension(ctx, args, options) {
8363
8615
  );
8364
8616
  }
8365
8617
  const primaryFile = primary.file;
8366
- const primaryPath = path46.join(ctx.scanPath, primaryFile);
8618
+ const primaryPath = path47.join(ctx.scanPath, primaryFile);
8367
8619
  const filesTouched = [];
8368
8620
  const depsAdded = [];
8369
- const pkgPath = path46.join(ctx.scanPath, "package.json");
8621
+ const pkgPath = path47.join(ctx.scanPath, "package.json");
8370
8622
  const pkg = await readPackageJson(ctx.scanPath);
8371
8623
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
8372
8624
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
@@ -8405,7 +8657,7 @@ async function dryRunExtension(ctx, args) {
8405
8657
  };
8406
8658
  }
8407
8659
  for (const file of hookFiles) {
8408
- const content = await fs27.readFile(path46.join(ctx.scanPath, file), "utf8");
8660
+ const content = await fs27.readFile(path47.join(ctx.scanPath, file), "utf8");
8409
8661
  if (content.includes(args.registration_snippet)) {
8410
8662
  return {
8411
8663
  library: args.library,
@@ -8446,7 +8698,7 @@ async function rollbackExtension(ctx, args) {
8446
8698
  if (!match) {
8447
8699
  return { undone: false, message: "no apply found for library" };
8448
8700
  }
8449
- const pkgPath = path46.join(ctx.scanPath, "package.json");
8701
+ const pkgPath = path47.join(ctx.scanPath, "package.json");
8450
8702
  if (await fileExists2(pkgPath)) {
8451
8703
  const pkg = await readPackageJson(ctx.scanPath);
8452
8704
  if (pkg.dependencies?.[match.instrumentation_package]) {
@@ -8457,7 +8709,7 @@ async function rollbackExtension(ctx, args) {
8457
8709
  }
8458
8710
  const hookFiles = await findHookFiles(ctx.scanPath);
8459
8711
  for (const file of hookFiles) {
8460
- const filePath = path46.join(ctx.scanPath, file);
8712
+ const filePath = path47.join(ctx.scanPath, file);
8461
8713
  const content = await fs27.readFile(filePath, "utf8");
8462
8714
  if (content.includes(match.registration_snippet)) {
8463
8715
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
@@ -8572,7 +8824,7 @@ data: ${JSON.stringify(envelope.payload)}
8572
8824
 
8573
8825
  // src/connectors-config.ts
8574
8826
  import os4 from "os";
8575
- import path47 from "path";
8827
+ import path48 from "path";
8576
8828
  import { promises as fs28 } from "fs";
8577
8829
  var CONNECTORS_CONFIG_VERSION = 1;
8578
8830
  var EnvRefUnsetError = class extends Error {
@@ -8587,11 +8839,11 @@ var EnvRefUnsetError = class extends Error {
8587
8839
  };
8588
8840
  function neatHome2() {
8589
8841
  const override = process.env.NEAT_HOME;
8590
- if (override && override.length > 0) return path47.resolve(override);
8591
- return path47.join(os4.homedir(), ".neat");
8842
+ if (override && override.length > 0) return path48.resolve(override);
8843
+ return path48.join(os4.homedir(), ".neat");
8592
8844
  }
8593
8845
  function connectorsConfigPath(home = neatHome2()) {
8594
- return path47.join(home, "connectors.json");
8846
+ return path48.join(home, "connectors.json");
8595
8847
  }
8596
8848
  var MODE_MASK_LOOSER_THAN_0600 = 63;
8597
8849
  async function warnIfModeLooserThan0600(file) {
@@ -8722,7 +8974,7 @@ function connectorMatchesProject(entry, project) {
8722
8974
  var CONNECTORS_LOCK_TIMEOUT_MS = 5e3;
8723
8975
  var CONNECTORS_LOCK_RETRY_MS = 50;
8724
8976
  function connectorsConfigLockPath(home = neatHome2()) {
8725
- return path47.join(home, "connectors.json.lock");
8977
+ return path48.join(home, "connectors.json.lock");
8726
8978
  }
8727
8979
  function isEnvRef(value) {
8728
8980
  return value.length > 1 && value.startsWith("$");
@@ -8735,7 +8987,7 @@ function redactCredentialRef(ref) {
8735
8987
  return out;
8736
8988
  }
8737
8989
  async function writeConfigAtomically0600(file, contents) {
8738
- await fs28.mkdir(path47.dirname(file), { recursive: true });
8990
+ await fs28.mkdir(path48.dirname(file), { recursive: true });
8739
8991
  const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
8740
8992
  const fd = await fs28.open(tmp, "w", 384);
8741
8993
  try {
@@ -8749,7 +9001,7 @@ async function writeConfigAtomically0600(file, contents) {
8749
9001
  }
8750
9002
  async function acquireConnectorsLock(lockPath, timeoutMs = CONNECTORS_LOCK_TIMEOUT_MS) {
8751
9003
  const deadline = Date.now() + timeoutMs;
8752
- await fs28.mkdir(path47.dirname(lockPath), { recursive: true });
9004
+ await fs28.mkdir(path48.dirname(lockPath), { recursive: true });
8753
9005
  for (; ; ) {
8754
9006
  try {
8755
9007
  const fd = await fs28.open(lockPath, "wx");
@@ -9743,4 +9995,4 @@ export {
9743
9995
  recordConnectorPoll,
9744
9996
  buildApi
9745
9997
  };
9746
- //# sourceMappingURL=chunk-CS4GHQO3.js.map
9998
+ //# sourceMappingURL=chunk-ZZ3VUCWL.js.map