@neat.is/core 0.6.1 → 0.6.2-dev.20260723

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,
@@ -2336,311 +1852,826 @@ async function handleSpan(ctx, span) {
2336
1852
  resolvedViaAddress = true;
2337
1853
  }
2338
1854
  }
2339
- if (!resolvedViaAddress && span.parentSpanId) {
2340
- const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
2341
- if (parent && parent.service !== span.service) {
2342
- const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
2343
- const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
2344
- const fallbackEvidence = parent.callSite ? {
2345
- file: reconcileObservedRelPath(
2346
- ctx.graph,
2347
- parent.service,
2348
- parent.callSite.relPath
2349
- ),
2350
- ...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
2351
- } : void 0;
2352
- upsertObservedEdge(
2353
- ctx.graph,
2354
- EdgeType3.CALLS,
2355
- fallbackSource,
2356
- sourceId,
2357
- ts,
2358
- isError,
2359
- fallbackEvidence
2360
- );
2361
- }
1855
+ if (!resolvedViaAddress && span.parentSpanId) {
1856
+ const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
1857
+ if (parent && parent.service !== span.service) {
1858
+ const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
1859
+ const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
1860
+ const fallbackEvidence = parent.callSite ? {
1861
+ file: reconcileObservedRelPath(
1862
+ ctx.graph,
1863
+ parent.service,
1864
+ parent.callSite.relPath
1865
+ ),
1866
+ ...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
1867
+ } : void 0;
1868
+ upsertObservedEdge(
1869
+ ctx.graph,
1870
+ EdgeType2.CALLS,
1871
+ fallbackSource,
1872
+ sourceId,
1873
+ ts,
1874
+ isError,
1875
+ fallbackEvidence
1876
+ );
1877
+ }
1878
+ }
1879
+ }
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);
2362
2206
  }
2363
2207
  }
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);
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);
2381
2218
  }
2382
2219
  }
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);
2220
+ return best;
2221
+ }
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] };
2282
+ }
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);
2391
2294
  }
2392
2295
  }
2393
- void affectedNode;
2394
- if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
2296
+ step(start, [start], []);
2297
+ return best;
2395
2298
  }
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);
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
+ }
2404
2326
  }
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;
2327
+ }
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
+ };
2421
2362
  }
2422
2363
  }
2423
- rewireFrontierEdges(graph, frontierId2, serviceId5);
2424
- graph.dropNode(frontierId2);
2425
- promoted++;
2426
2364
  }
2427
- return promoted;
2365
+ return null;
2428
2366
  }
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);
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
+ }
2435
2395
  }
2436
- for (const edgeId of outbound) {
2437
- const edge = graph.getEdgeAttributes(edgeId);
2438
- rebuildEdge(graph, edge, serviceId5, edge.target, edgeId);
2396
+ if (origin.type === NodeType3.ServiceNode) {
2397
+ const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
2398
+ if (crossService) return crossService;
2439
2399
  }
2400
+ return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
2440
2401
  }
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;
2453
- }
2454
- const rebuilt = {
2455
- ...edge,
2456
- id: newId,
2457
- source: newSource,
2458
- target: newTarget
2402
+ var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
2403
+ function incidentMatchesNode(ev, nodeId) {
2404
+ return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
2405
+ }
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 } : {}
2459
2430
  };
2460
- graph.addEdgeWithKey(newId, newSource, newTarget, rebuilt);
2461
2431
  }
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;
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 } : {}
2444
+ });
2466
2445
  }
2467
- function makeSpanHandler(ctx) {
2468
- return (span) => handleSpan(ctx, span);
2446
+ function isFailingCallEdge(e) {
2447
+ return e.type === EdgeType3.CALLS && (e.signal?.errorCount ?? 0) > 0;
2469
2448
  }
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
- }
2504
- });
2505
- if (options.staleEventsPath && events.length > 0) {
2506
- await appendStaleEvents(options.staleEventsPath, events);
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);
2507
2456
  }
2508
- return { count: events.length, events };
2509
- }
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");
2457
+ return ids;
2514
2458
  }
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;
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];
2522
2465
  }
2466
+ return id < curId;
2523
2467
  }
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);
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;
2496
+ }
2497
+ if (edges.length === 0) return null;
2498
+ return { path: path49, edges, culprit: current };
2499
+ }
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
+ });
2536
+ }
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
+ });
2555
+ }
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
+ }
2557
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
+ });
2558
2577
  }
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;
2565
- }
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;
2574
- }
2575
- if (seen.has(key)) continue;
2576
- seen.add(key);
2577
- once.push(ev);
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) {
@@ -5140,20 +5171,20 @@ function grpcMethodsFromProto(content, fqPackage) {
5140
5171
  }
5141
5172
  async function walkProtoFiles(dir) {
5142
5173
  const out = [];
5143
- async function walk3(current) {
5174
+ async function walk4(current) {
5144
5175
  const entries = await fs15.readdir(current, { withFileTypes: true }).catch(() => []);
5145
5176
  for (const entry of entries) {
5146
5177
  const full = path23.join(current, entry.name);
5147
5178
  if (entry.isDirectory()) {
5148
5179
  if (IGNORED_DIRS.has(entry.name)) continue;
5149
5180
  if (await isPythonVenvDir(full)) continue;
5150
- await walk3(full);
5181
+ await walk4(full);
5151
5182
  } else if (entry.isFile() && path23.extname(entry.name) === PROTO_EXTENSION) {
5152
5183
  out.push(full);
5153
5184
  }
5154
5185
  }
5155
5186
  }
5156
- await walk3(dir);
5187
+ await walk4(dir);
5157
5188
  return out;
5158
5189
  }
5159
5190
  async function addGrpcMethods(graph, services) {
@@ -6239,6 +6270,120 @@ async function mongooseCrossFileEndpoints(files, serviceDir) {
6239
6270
  return out;
6240
6271
  }
6241
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
+
6242
6387
  // src/extract/calls/index.ts
6243
6388
  function edgeTypeFromEndpoint(ep) {
6244
6389
  switch (ep.edgeType) {
@@ -6271,6 +6416,7 @@ async function addExternalEndpointEdges(graph, services) {
6271
6416
  endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
6272
6417
  endpoints.push(...supabaseEndpointsFromFile(maskedFile, service.dir));
6273
6418
  endpoints.push(...mongooseEndpointsFromFile(maskedFile, service.dir));
6419
+ endpoints.push(...sqlalchemyEndpointsFromFile(maskedFile, service.dir));
6274
6420
  }
6275
6421
  endpoints.push(...await mongooseCrossFileEndpoints(maskedFiles, service.dir));
6276
6422
  if (endpoints.length === 0) continue;
@@ -6343,14 +6489,14 @@ async function addCallEdges(graph, services) {
6343
6489
  }
6344
6490
 
6345
6491
  // src/extract/infra/docker-compose.ts
6346
- import path32 from "path";
6492
+ import path33 from "path";
6347
6493
  import { EdgeType as EdgeType13, Provenance as Provenance13, confidenceForExtracted as confidenceForExtracted11 } from "@neat.is/types";
6348
6494
 
6349
6495
  // src/extract/infra/shared.ts
6350
- 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";
6351
6497
  function makeInfraNode(kind, name, provider = "self", extras) {
6352
6498
  return {
6353
- id: infraId8(kind, name),
6499
+ id: infraId9(kind, name),
6354
6500
  type: NodeType13.InfraNode,
6355
6501
  name,
6356
6502
  provider,
@@ -6413,7 +6559,7 @@ function dependsOnList(value) {
6413
6559
  }
6414
6560
  function serviceNameToServiceNode(name, services) {
6415
6561
  for (const s of services) {
6416
- 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;
6417
6563
  }
6418
6564
  return null;
6419
6565
  }
@@ -6422,7 +6568,7 @@ async function addComposeInfra(graph, scanPath, services) {
6422
6568
  let edgesAdded = 0;
6423
6569
  let composePath = null;
6424
6570
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
6425
- const abs = path32.join(scanPath, name);
6571
+ const abs = path33.join(scanPath, name);
6426
6572
  if (await exists(abs)) {
6427
6573
  composePath = abs;
6428
6574
  break;
@@ -6435,13 +6581,13 @@ async function addComposeInfra(graph, scanPath, services) {
6435
6581
  } catch (err) {
6436
6582
  recordExtractionError(
6437
6583
  "infra docker-compose",
6438
- path32.relative(scanPath, composePath),
6584
+ path33.relative(scanPath, composePath),
6439
6585
  err
6440
6586
  );
6441
6587
  return { nodesAdded, edgesAdded };
6442
6588
  }
6443
6589
  if (!compose?.services) return { nodesAdded, edgesAdded };
6444
- const evidenceFile = path32.relative(scanPath, composePath).split(path32.sep).join("/");
6590
+ const evidenceFile = path33.relative(scanPath, composePath).split(path33.sep).join("/");
6445
6591
  const composeNameToNodeId = /* @__PURE__ */ new Map();
6446
6592
  for (const [composeName, svc] of Object.entries(compose.services)) {
6447
6593
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -6482,7 +6628,7 @@ async function addComposeInfra(graph, scanPath, services) {
6482
6628
  }
6483
6629
 
6484
6630
  // src/extract/infra/dockerfile.ts
6485
- import path33 from "path";
6631
+ import path34 from "path";
6486
6632
  import { promises as fs16 } from "fs";
6487
6633
  import { EdgeType as EdgeType14, Provenance as Provenance14, confidenceForExtracted as confidenceForExtracted12 } from "@neat.is/types";
6488
6634
  function readDockerfile(content) {
@@ -6513,7 +6659,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6513
6659
  let nodesAdded = 0;
6514
6660
  let edgesAdded = 0;
6515
6661
  for (const service of services) {
6516
- const dockerfilePath = path33.join(service.dir, "Dockerfile");
6662
+ const dockerfilePath = path34.join(service.dir, "Dockerfile");
6517
6663
  if (!await exists(dockerfilePath)) continue;
6518
6664
  let content;
6519
6665
  try {
@@ -6521,7 +6667,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6521
6667
  } catch (err) {
6522
6668
  recordExtractionError(
6523
6669
  "infra dockerfile",
6524
- path33.relative(scanPath, dockerfilePath),
6670
+ path34.relative(scanPath, dockerfilePath),
6525
6671
  err
6526
6672
  );
6527
6673
  continue;
@@ -6533,8 +6679,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6533
6679
  graph.addNode(node.id, node);
6534
6680
  nodesAdded++;
6535
6681
  }
6536
- const relDockerfile = toPosix2(path33.relative(service.dir, dockerfilePath));
6537
- const evidenceFile = toPosix2(path33.relative(scanPath, dockerfilePath));
6682
+ const relDockerfile = toPosix2(path34.relative(service.dir, dockerfilePath));
6683
+ const evidenceFile = toPosix2(path34.relative(scanPath, dockerfilePath));
6538
6684
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
6539
6685
  graph,
6540
6686
  service.pkg.name,
@@ -6586,7 +6732,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
6586
6732
 
6587
6733
  // src/extract/infra/terraform.ts
6588
6734
  import { promises as fs17 } from "fs";
6589
- import path34 from "path";
6735
+ import path35 from "path";
6590
6736
  import { EdgeType as EdgeType15, Provenance as Provenance15, confidenceForExtracted as confidenceForExtracted13 } from "@neat.is/types";
6591
6737
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
6592
6738
  var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
@@ -6597,11 +6743,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
6597
6743
  for (const entry of entries) {
6598
6744
  if (entry.isDirectory()) {
6599
6745
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
6600
- const child = path34.join(start, entry.name);
6746
+ const child = path35.join(start, entry.name);
6601
6747
  if (await isPythonVenvDir(child)) continue;
6602
6748
  out.push(...await walkTfFiles(child, depth + 1, max));
6603
6749
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
6604
- out.push(path34.join(start, entry.name));
6750
+ out.push(path35.join(start, entry.name));
6605
6751
  }
6606
6752
  }
6607
6753
  return out;
@@ -6633,7 +6779,7 @@ async function addTerraformResources(graph, scanPath) {
6633
6779
  const files = await walkTfFiles(scanPath);
6634
6780
  for (const file of files) {
6635
6781
  const content = await fs17.readFile(file, "utf8");
6636
- const evidenceFile = toPosix2(path34.relative(scanPath, file));
6782
+ const evidenceFile = toPosix2(path35.relative(scanPath, file));
6637
6783
  const resources = [];
6638
6784
  const byKey = /* @__PURE__ */ new Map();
6639
6785
  RESOURCE_RE.lastIndex = 0;
@@ -6690,7 +6836,7 @@ async function addTerraformResources(graph, scanPath) {
6690
6836
 
6691
6837
  // src/extract/infra/k8s.ts
6692
6838
  import { promises as fs18 } from "fs";
6693
- import path35 from "path";
6839
+ import path36 from "path";
6694
6840
  import { parseAllDocuments as parseAllDocuments2 } from "yaml";
6695
6841
  var K8S_KIND_TO_INFRA_KIND = {
6696
6842
  Service: "k8s-service",
@@ -6708,11 +6854,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
6708
6854
  for (const entry of entries) {
6709
6855
  if (entry.isDirectory()) {
6710
6856
  if (IGNORED_DIRS.has(entry.name)) continue;
6711
- const child = path35.join(start, entry.name);
6857
+ const child = path36.join(start, entry.name);
6712
6858
  if (await isPythonVenvDir(child)) continue;
6713
6859
  out.push(...await walkYamlFiles2(child, depth + 1, max));
6714
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path35.extname(entry.name))) {
6715
- 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));
6716
6862
  }
6717
6863
  }
6718
6864
  return out;
@@ -6745,13 +6891,13 @@ async function addK8sResources(graph, scanPath) {
6745
6891
 
6746
6892
  // src/extract/infra/cloudflare.ts
6747
6893
  import { promises as fs19 } from "fs";
6748
- import path36 from "path";
6894
+ import path37 from "path";
6749
6895
  import { parse as parseToml2 } from "smol-toml";
6750
6896
  import { EdgeType as EdgeType16, Provenance as Provenance16, confidenceForExtracted as confidenceForExtracted14 } from "@neat.is/types";
6751
6897
  var WRANGLER_FILENAMES = ["wrangler.toml", "wrangler.jsonc", "wrangler.json"];
6752
6898
  async function readWranglerConfig(dir) {
6753
6899
  for (const filename of WRANGLER_FILENAMES) {
6754
- const abs = path36.join(dir, filename);
6900
+ const abs = path37.join(dir, filename);
6755
6901
  if (!await exists(abs)) continue;
6756
6902
  const raw = await fs19.readFile(abs, "utf8");
6757
6903
  const config = filename === "wrangler.toml" ? parseToml2(raw) : JSON.parse(maskCommentsInSource(raw));
@@ -6814,11 +6960,11 @@ async function addCloudflareWorkers(graph, services, scanPath) {
6814
6960
  try {
6815
6961
  read = await readWranglerConfig(service.dir);
6816
6962
  } catch (err) {
6817
- recordExtractionError("infra cloudflare", path36.relative(scanPath, service.dir), err);
6963
+ recordExtractionError("infra cloudflare", path37.relative(scanPath, service.dir), err);
6818
6964
  continue;
6819
6965
  }
6820
6966
  if (!read || !read.config.name) continue;
6821
- 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)));
6822
6968
  discovered.push({ service, config: read.config, relFile: read.relFile, raw: read.raw, evidenceFile });
6823
6969
  }
6824
6970
  for (const worker of discovered) {
@@ -6830,7 +6976,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
6830
6976
  }
6831
6977
  let anchorId = service.node.id;
6832
6978
  if (config.main) {
6833
- const entryRelPath = toPosix2(path36.normalize(config.main));
6979
+ const entryRelPath = toPosix2(path37.normalize(config.main));
6834
6980
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
6835
6981
  graph,
6836
6982
  service.pkg.name,
@@ -6977,12 +7123,12 @@ async function addCloudflareWorkers(graph, services, scanPath) {
6977
7123
 
6978
7124
  // src/extract/infra/vercel.ts
6979
7125
  import { promises as fs20 } from "fs";
6980
- import path37 from "path";
7126
+ import path38 from "path";
6981
7127
  import { EdgeType as EdgeType17 } from "@neat.is/types";
6982
7128
  var VERCEL_CONFIG_FILENAMES = ["vercel.json", "vercel.jsonc"];
6983
7129
  async function readVercelConfig(dir) {
6984
7130
  for (const filename of VERCEL_CONFIG_FILENAMES) {
6985
- const abs = path37.join(dir, filename);
7131
+ const abs = path38.join(dir, filename);
6986
7132
  if (!await exists(abs)) continue;
6987
7133
  const raw = await fs20.readFile(abs, "utf8");
6988
7134
  const config = JSON.parse(maskCommentsInSource(raw));
@@ -6991,7 +7137,7 @@ async function readVercelConfig(dir) {
6991
7137
  return null;
6992
7138
  }
6993
7139
  async function readLinkedProjectName(dir) {
6994
- const abs = path37.join(dir, ".vercel", "project.json");
7140
+ const abs = path38.join(dir, ".vercel", "project.json");
6995
7141
  if (!await exists(abs)) return void 0;
6996
7142
  const parsed = JSON.parse(await fs20.readFile(abs, "utf8"));
6997
7143
  return typeof parsed.projectName === "string" ? parsed.projectName : void 0;
@@ -7009,7 +7155,7 @@ async function addVercelServices(graph, services, scanPath) {
7009
7155
  read = await readVercelConfig(service.dir);
7010
7156
  projectName = await readLinkedProjectName(service.dir);
7011
7157
  } catch (err) {
7012
- recordExtractionError("infra vercel", path37.relative(scanPath, service.dir), err);
7158
+ recordExtractionError("infra vercel", path38.relative(scanPath, service.dir), err);
7013
7159
  continue;
7014
7160
  }
7015
7161
  if (!read && !projectName) continue;
@@ -7025,7 +7171,7 @@ async function addVercelServices(graph, services, scanPath) {
7025
7171
  const anchorId = service.node.id;
7026
7172
  if (!read) continue;
7027
7173
  const { config, relFile, raw } = read;
7028
- const evidenceFile = toPosix2(path37.relative(scanPath, path37.join(service.dir, relFile)));
7174
+ const evidenceFile = toPosix2(path38.relative(scanPath, path38.join(service.dir, relFile)));
7029
7175
  const add = (edgeType, kind, name) => {
7030
7176
  if (!name) return;
7031
7177
  const result = emitPlatformResourceEdge(
@@ -7054,13 +7200,13 @@ async function addVercelServices(graph, services, scanPath) {
7054
7200
 
7055
7201
  // src/extract/infra/railway.ts
7056
7202
  import { promises as fs21 } from "fs";
7057
- import path38 from "path";
7203
+ import path39 from "path";
7058
7204
  import { parse as parseToml3 } from "smol-toml";
7059
7205
  import { EdgeType as EdgeType18 } from "@neat.is/types";
7060
7206
  var RAILWAY_FILENAMES = ["railway.toml", "railway.json", "railway.jsonc"];
7061
7207
  async function readRailwayConfig(dir) {
7062
7208
  for (const filename of RAILWAY_FILENAMES) {
7063
- const abs = path38.join(dir, filename);
7209
+ const abs = path39.join(dir, filename);
7064
7210
  if (!await exists(abs)) continue;
7065
7211
  const raw = await fs21.readFile(abs, "utf8");
7066
7212
  const config = filename === "railway.toml" ? parseToml3(raw) : JSON.parse(maskCommentsInSource(raw));
@@ -7076,7 +7222,7 @@ async function addRailwayServices(graph, services, scanPath) {
7076
7222
  try {
7077
7223
  read = await readRailwayConfig(service.dir);
7078
7224
  } catch (err) {
7079
- recordExtractionError("infra railway", path38.relative(scanPath, service.dir), err);
7225
+ recordExtractionError("infra railway", path39.relative(scanPath, service.dir), err);
7080
7226
  continue;
7081
7227
  }
7082
7228
  if (!read) continue;
@@ -7086,7 +7232,7 @@ async function addRailwayServices(graph, services, scanPath) {
7086
7232
  }
7087
7233
  const anchorId = service.node.id;
7088
7234
  const { config, relFile, raw } = read;
7089
- const evidenceFile = toPosix2(path38.relative(scanPath, path38.join(service.dir, relFile)));
7235
+ const evidenceFile = toPosix2(path39.relative(scanPath, path39.join(service.dir, relFile)));
7090
7236
  const add = (edgeType, kind, name) => {
7091
7237
  if (!name) return;
7092
7238
  const result = emitPlatformResourceEdge(
@@ -7111,12 +7257,12 @@ async function addRailwayServices(graph, services, scanPath) {
7111
7257
 
7112
7258
  // src/extract/infra/supabase.ts
7113
7259
  import { promises as fs22 } from "fs";
7114
- import path39 from "path";
7260
+ import path40 from "path";
7115
7261
  import { parse as parseToml4 } from "smol-toml";
7116
7262
  import { EdgeType as EdgeType19 } from "@neat.is/types";
7117
7263
  async function readSupabaseConfig(dir) {
7118
- const relFile = path39.join("supabase", "config.toml");
7119
- const abs = path39.join(dir, relFile);
7264
+ const relFile = path40.join("supabase", "config.toml");
7265
+ const abs = path40.join(dir, relFile);
7120
7266
  if (!await exists(abs)) return null;
7121
7267
  const raw = await fs22.readFile(abs, "utf8");
7122
7268
  const config = parseToml4(raw);
@@ -7130,7 +7276,7 @@ async function addSupabaseProjects(graph, services, scanPath) {
7130
7276
  try {
7131
7277
  read = await readSupabaseConfig(service.dir);
7132
7278
  } catch (err) {
7133
- recordExtractionError("infra supabase", path39.relative(scanPath, service.dir), err);
7279
+ recordExtractionError("infra supabase", path40.relative(scanPath, service.dir), err);
7134
7280
  continue;
7135
7281
  }
7136
7282
  if (!read) continue;
@@ -7145,7 +7291,7 @@ async function addSupabaseProjects(graph, services, scanPath) {
7145
7291
  });
7146
7292
  }
7147
7293
  const anchorId = service.node.id;
7148
- const evidenceFile = toPosix2(path39.relative(scanPath, path39.join(service.dir, relFile)));
7294
+ const evidenceFile = toPosix2(path40.relative(scanPath, path40.join(service.dir, relFile)));
7149
7295
  const add = (edgeType, kind, name) => {
7150
7296
  if (!name) return;
7151
7297
  const result = emitPlatformResourceEdge(
@@ -7186,11 +7332,11 @@ async function addInfra(graph, scanPath, services) {
7186
7332
  }
7187
7333
 
7188
7334
  // src/extract/index.ts
7189
- import path41 from "path";
7335
+ import path42 from "path";
7190
7336
 
7191
7337
  // src/extract/retire.ts
7192
7338
  import { existsSync as existsSync2 } from "fs";
7193
- import path40 from "path";
7339
+ import path41 from "path";
7194
7340
  import { NodeType as NodeType14, Provenance as Provenance17 } from "@neat.is/types";
7195
7341
  function dropOrphanedFileNodes(graph) {
7196
7342
  const orphans = [];
@@ -7224,11 +7370,11 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
7224
7370
  if (edge.provenance !== Provenance17.EXTRACTED) return;
7225
7371
  const evidenceFile = edge.evidence?.file;
7226
7372
  if (!evidenceFile) return;
7227
- if (path40.isAbsolute(evidenceFile)) {
7373
+ if (path41.isAbsolute(evidenceFile)) {
7228
7374
  if (!existsSync2(evidenceFile)) toDrop.push(id);
7229
7375
  return;
7230
7376
  }
7231
- const found = bases.some((base) => existsSync2(path40.join(base, evidenceFile)));
7377
+ const found = bases.some((base) => existsSync2(path41.join(base, evidenceFile)));
7232
7378
  if (!found) toDrop.push(id);
7233
7379
  });
7234
7380
  for (const id of toDrop) graph.dropEdge(id);
@@ -7270,7 +7416,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
7270
7416
  }
7271
7417
  const droppedEntries = drainDroppedExtracted();
7272
7418
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
7273
- const rejectedPath = path41.join(path41.dirname(opts.errorsPath), "rejected.ndjson");
7419
+ const rejectedPath = path42.join(path42.dirname(opts.errorsPath), "rejected.ndjson");
7274
7420
  try {
7275
7421
  await writeRejectedExtracted(droppedEntries, rejectedPath);
7276
7422
  } catch (err) {
@@ -7588,7 +7734,7 @@ function computeDivergences(graph, opts = {}) {
7588
7734
 
7589
7735
  // src/persist.ts
7590
7736
  import { promises as fs23 } from "fs";
7591
- import path42 from "path";
7737
+ import path43 from "path";
7592
7738
  import { Provenance as Provenance19, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
7593
7739
  var SCHEMA_VERSION = 4;
7594
7740
  function migrateV1ToV2(payload) {
@@ -7625,7 +7771,7 @@ function migrateV2ToV3(payload) {
7625
7771
  return { ...payload, schemaVersion: 3 };
7626
7772
  }
7627
7773
  async function ensureDir(filePath) {
7628
- await fs23.mkdir(path42.dirname(filePath), { recursive: true });
7774
+ await fs23.mkdir(path43.dirname(filePath), { recursive: true });
7629
7775
  }
7630
7776
  async function saveGraphToDisk(graph, outPath) {
7631
7777
  await ensureDir(outPath);
@@ -7781,23 +7927,23 @@ function canonicalJson(value) {
7781
7927
  }
7782
7928
 
7783
7929
  // src/projects.ts
7784
- import path43 from "path";
7930
+ import path44 from "path";
7785
7931
  function pathsForProject(project, baseDir) {
7786
7932
  if (project === DEFAULT_PROJECT) {
7787
7933
  return {
7788
- snapshotPath: path43.join(baseDir, "graph.json"),
7789
- errorsPath: path43.join(baseDir, "errors.ndjson"),
7790
- staleEventsPath: path43.join(baseDir, "stale-events.ndjson"),
7791
- embeddingsCachePath: path43.join(baseDir, "embeddings.json"),
7792
- 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")
7793
7939
  };
7794
7940
  }
7795
7941
  return {
7796
- snapshotPath: path43.join(baseDir, `${project}.json`),
7797
- errorsPath: path43.join(baseDir, `errors.${project}.ndjson`),
7798
- staleEventsPath: path43.join(baseDir, `stale-events.${project}.ndjson`),
7799
- embeddingsCachePath: path43.join(baseDir, `embeddings.${project}.json`),
7800
- 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`)
7801
7947
  };
7802
7948
  }
7803
7949
  var Projects = class {
@@ -7838,7 +7984,7 @@ function parseExtraProjects(raw) {
7838
7984
  // src/registry.ts
7839
7985
  import { promises as fs25 } from "fs";
7840
7986
  import os2 from "os";
7841
- import path44 from "path";
7987
+ import path45 from "path";
7842
7988
  import {
7843
7989
  RegistryFileSchema
7844
7990
  } from "@neat.is/types";
@@ -7846,20 +7992,20 @@ var LOCK_TIMEOUT_MS = 5e3;
7846
7992
  var LOCK_RETRY_MS = 50;
7847
7993
  function neatHome() {
7848
7994
  const override = process.env.NEAT_HOME;
7849
- if (override && override.length > 0) return path44.resolve(override);
7850
- return path44.join(os2.homedir(), ".neat");
7995
+ if (override && override.length > 0) return path45.resolve(override);
7996
+ return path45.join(os2.homedir(), ".neat");
7851
7997
  }
7852
7998
  function registryPath() {
7853
- return path44.join(neatHome(), "projects.json");
7999
+ return path45.join(neatHome(), "projects.json");
7854
8000
  }
7855
8001
  function registryLockPath() {
7856
- return path44.join(neatHome(), "projects.json.lock");
8002
+ return path45.join(neatHome(), "projects.json.lock");
7857
8003
  }
7858
8004
  function daemonPidPath() {
7859
- return path44.join(neatHome(), "neatd.pid");
8005
+ return path45.join(neatHome(), "neatd.pid");
7860
8006
  }
7861
8007
  function daemonsDir() {
7862
- return path44.join(neatHome(), "daemons");
8008
+ return path45.join(neatHome(), "daemons");
7863
8009
  }
7864
8010
  function isFiniteInt(v) {
7865
8011
  return typeof v === "number" && Number.isFinite(v);
@@ -7900,7 +8046,7 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
7900
8046
  const out = [];
7901
8047
  for (const name of names) {
7902
8048
  if (!name.endsWith(".json")) continue;
7903
- const file = path44.join(dir, name);
8049
+ const file = path45.join(dir, name);
7904
8050
  let raw;
7905
8051
  try {
7906
8052
  raw = await fs25.readFile(file, "utf8");
@@ -8021,7 +8167,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
8021
8167
  }
8022
8168
  }
8023
8169
  async function normalizeProjectPath(input) {
8024
- const resolved = path44.resolve(input);
8170
+ const resolved = path45.resolve(input);
8025
8171
  try {
8026
8172
  return await fs25.realpath(resolved);
8027
8173
  } catch {
@@ -8029,7 +8175,7 @@ async function normalizeProjectPath(input) {
8029
8175
  }
8030
8176
  }
8031
8177
  async function writeAtomically(target, contents) {
8032
- await fs25.mkdir(path44.dirname(target), { recursive: true });
8178
+ await fs25.mkdir(path45.dirname(target), { recursive: true });
8033
8179
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
8034
8180
  const fd = await fs25.open(tmp, "w");
8035
8181
  try {
@@ -8042,7 +8188,7 @@ async function writeAtomically(target, contents) {
8042
8188
  }
8043
8189
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
8044
8190
  const deadline = Date.now() + timeoutMs;
8045
- await fs25.mkdir(path44.dirname(lockPath), { recursive: true });
8191
+ await fs25.mkdir(path45.dirname(lockPath), { recursive: true });
8046
8192
  let probedHolder = false;
8047
8193
  while (true) {
8048
8194
  try {
@@ -8238,13 +8384,13 @@ import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } f
8238
8384
 
8239
8385
  // src/extend/index.ts
8240
8386
  import { promises as fs27 } from "fs";
8241
- import path46 from "path";
8387
+ import path47 from "path";
8242
8388
  import os3 from "os";
8243
8389
  import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
8244
8390
 
8245
8391
  // src/installers/package-manager.ts
8246
8392
  import { promises as fs26 } from "fs";
8247
- import path45 from "path";
8393
+ import path46 from "path";
8248
8394
  import { spawn } from "child_process";
8249
8395
  var LOCKFILE_PRIORITY = [
8250
8396
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -8266,22 +8412,22 @@ async function exists2(p) {
8266
8412
  }
8267
8413
  }
8268
8414
  async function detectPackageManager(serviceDir) {
8269
- let dir = path45.resolve(serviceDir);
8415
+ let dir = path46.resolve(serviceDir);
8270
8416
  const stops = /* @__PURE__ */ new Set();
8271
8417
  for (let i = 0; i < 64; i++) {
8272
8418
  if (stops.has(dir)) break;
8273
8419
  stops.add(dir);
8274
8420
  for (const candidate of LOCKFILE_PRIORITY) {
8275
- const lockPath = path45.join(dir, candidate.lockfile);
8421
+ const lockPath = path46.join(dir, candidate.lockfile);
8276
8422
  if (await exists2(lockPath)) {
8277
8423
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
8278
8424
  }
8279
8425
  }
8280
- const parent = path45.dirname(dir);
8426
+ const parent = path46.dirname(dir);
8281
8427
  if (parent === dir) break;
8282
8428
  dir = parent;
8283
8429
  }
8284
- return { pm: "npm", cwd: path45.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
8430
+ return { pm: "npm", cwd: path46.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
8285
8431
  }
8286
8432
  async function runPackageManagerInstall(cmd) {
8287
8433
  return new Promise((resolve) => {
@@ -8330,7 +8476,7 @@ async function fileExists2(p) {
8330
8476
  }
8331
8477
  }
8332
8478
  async function readPackageJson(scanPath) {
8333
- const pkgPath = path46.join(scanPath, "package.json");
8479
+ const pkgPath = path47.join(scanPath, "package.json");
8334
8480
  const raw = await fs27.readFile(pkgPath, "utf8");
8335
8481
  return JSON.parse(raw);
8336
8482
  }
@@ -8344,27 +8490,27 @@ var HOOK_WALK_SKIP_DIRS = /* @__PURE__ */ new Set([
8344
8490
  ]);
8345
8491
  async function findHookFiles(scanPath) {
8346
8492
  const found = [];
8347
- const walk3 = async (dir) => {
8493
+ const walk4 = async (dir) => {
8348
8494
  const entries = await fs27.readdir(dir, { withFileTypes: true }).catch(() => []);
8349
8495
  for (const entry of entries) {
8350
8496
  if (entry.isDirectory()) {
8351
8497
  if (entry.name.startsWith(".") || HOOK_WALK_SKIP_DIRS.has(entry.name)) continue;
8352
- await walk3(path46.join(dir, entry.name));
8498
+ await walk4(path47.join(dir, entry.name));
8353
8499
  } else if (entry.isFile()) {
8354
8500
  if ((entry.name.startsWith("instrumentation") || entry.name.startsWith("otel-init")) && /\.(ts|js|cjs|mjs)$/.test(entry.name)) {
8355
- const rel = path46.relative(scanPath, path46.join(dir, entry.name));
8356
- 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("/"));
8357
8503
  }
8358
8504
  }
8359
8505
  }
8360
8506
  };
8361
- await walk3(scanPath);
8507
+ await walk4(scanPath);
8362
8508
  return found.sort();
8363
8509
  }
8364
8510
  async function pickPrimaryHookFile(scanPath, hookFiles, snippet2) {
8365
8511
  let fallback = null;
8366
8512
  for (const file of hookFiles) {
8367
- const content = await fs27.readFile(path46.join(scanPath, file), "utf8");
8513
+ const content = await fs27.readFile(path47.join(scanPath, file), "utf8");
8368
8514
  const patched = splicedContent(content, snippet2);
8369
8515
  if (patched !== null) return { file, content, patched };
8370
8516
  if (fallback === null) fallback = { file, content };
@@ -8372,11 +8518,11 @@ async function pickPrimaryHookFile(scanPath, hookFiles, snippet2) {
8372
8518
  return { file: fallback.file, content: fallback.content, patched: null };
8373
8519
  }
8374
8520
  function extendLogPath() {
8375
- 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");
8376
8522
  }
8377
8523
  async function appendExtendLog(entry) {
8378
8524
  const logPath = extendLogPath();
8379
- await fs27.mkdir(path46.dirname(logPath), { recursive: true });
8525
+ await fs27.mkdir(path47.dirname(logPath), { recursive: true });
8380
8526
  await fs27.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
8381
8527
  }
8382
8528
  function splicedContent(fileContent, snippet2) {
@@ -8435,7 +8581,7 @@ function lookupInstrumentation(library, installedVersion) {
8435
8581
  }
8436
8582
  async function describeProjectInstrumentation(ctx) {
8437
8583
  const hookFiles = await findHookFiles(ctx.scanPath);
8438
- const envNeat = await fileExists2(path46.join(ctx.scanPath, ".env.neat"));
8584
+ const envNeat = await fileExists2(path47.join(ctx.scanPath, ".env.neat"));
8439
8585
  const registryInstrPackages = new Set(
8440
8586
  registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
8441
8587
  );
@@ -8457,7 +8603,7 @@ async function applyExtension(ctx, args, options) {
8457
8603
  );
8458
8604
  }
8459
8605
  for (const file of hookFiles) {
8460
- const content = await fs27.readFile(path46.join(ctx.scanPath, file), "utf8");
8606
+ const content = await fs27.readFile(path47.join(ctx.scanPath, file), "utf8");
8461
8607
  if (content.includes(args.registration_snippet)) {
8462
8608
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
8463
8609
  }
@@ -8469,10 +8615,10 @@ async function applyExtension(ctx, args, options) {
8469
8615
  );
8470
8616
  }
8471
8617
  const primaryFile = primary.file;
8472
- const primaryPath = path46.join(ctx.scanPath, primaryFile);
8618
+ const primaryPath = path47.join(ctx.scanPath, primaryFile);
8473
8619
  const filesTouched = [];
8474
8620
  const depsAdded = [];
8475
- const pkgPath = path46.join(ctx.scanPath, "package.json");
8621
+ const pkgPath = path47.join(ctx.scanPath, "package.json");
8476
8622
  const pkg = await readPackageJson(ctx.scanPath);
8477
8623
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
8478
8624
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
@@ -8511,7 +8657,7 @@ async function dryRunExtension(ctx, args) {
8511
8657
  };
8512
8658
  }
8513
8659
  for (const file of hookFiles) {
8514
- const content = await fs27.readFile(path46.join(ctx.scanPath, file), "utf8");
8660
+ const content = await fs27.readFile(path47.join(ctx.scanPath, file), "utf8");
8515
8661
  if (content.includes(args.registration_snippet)) {
8516
8662
  return {
8517
8663
  library: args.library,
@@ -8552,7 +8698,7 @@ async function rollbackExtension(ctx, args) {
8552
8698
  if (!match) {
8553
8699
  return { undone: false, message: "no apply found for library" };
8554
8700
  }
8555
- const pkgPath = path46.join(ctx.scanPath, "package.json");
8701
+ const pkgPath = path47.join(ctx.scanPath, "package.json");
8556
8702
  if (await fileExists2(pkgPath)) {
8557
8703
  const pkg = await readPackageJson(ctx.scanPath);
8558
8704
  if (pkg.dependencies?.[match.instrumentation_package]) {
@@ -8563,7 +8709,7 @@ async function rollbackExtension(ctx, args) {
8563
8709
  }
8564
8710
  const hookFiles = await findHookFiles(ctx.scanPath);
8565
8711
  for (const file of hookFiles) {
8566
- const filePath = path46.join(ctx.scanPath, file);
8712
+ const filePath = path47.join(ctx.scanPath, file);
8567
8713
  const content = await fs27.readFile(filePath, "utf8");
8568
8714
  if (content.includes(match.registration_snippet)) {
8569
8715
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
@@ -8678,7 +8824,7 @@ data: ${JSON.stringify(envelope.payload)}
8678
8824
 
8679
8825
  // src/connectors-config.ts
8680
8826
  import os4 from "os";
8681
- import path47 from "path";
8827
+ import path48 from "path";
8682
8828
  import { promises as fs28 } from "fs";
8683
8829
  var CONNECTORS_CONFIG_VERSION = 1;
8684
8830
  var EnvRefUnsetError = class extends Error {
@@ -8693,11 +8839,11 @@ var EnvRefUnsetError = class extends Error {
8693
8839
  };
8694
8840
  function neatHome2() {
8695
8841
  const override = process.env.NEAT_HOME;
8696
- if (override && override.length > 0) return path47.resolve(override);
8697
- return path47.join(os4.homedir(), ".neat");
8842
+ if (override && override.length > 0) return path48.resolve(override);
8843
+ return path48.join(os4.homedir(), ".neat");
8698
8844
  }
8699
8845
  function connectorsConfigPath(home = neatHome2()) {
8700
- return path47.join(home, "connectors.json");
8846
+ return path48.join(home, "connectors.json");
8701
8847
  }
8702
8848
  var MODE_MASK_LOOSER_THAN_0600 = 63;
8703
8849
  async function warnIfModeLooserThan0600(file) {
@@ -8828,7 +8974,7 @@ function connectorMatchesProject(entry, project) {
8828
8974
  var CONNECTORS_LOCK_TIMEOUT_MS = 5e3;
8829
8975
  var CONNECTORS_LOCK_RETRY_MS = 50;
8830
8976
  function connectorsConfigLockPath(home = neatHome2()) {
8831
- return path47.join(home, "connectors.json.lock");
8977
+ return path48.join(home, "connectors.json.lock");
8832
8978
  }
8833
8979
  function isEnvRef(value) {
8834
8980
  return value.length > 1 && value.startsWith("$");
@@ -8841,7 +8987,7 @@ function redactCredentialRef(ref) {
8841
8987
  return out;
8842
8988
  }
8843
8989
  async function writeConfigAtomically0600(file, contents) {
8844
- await fs28.mkdir(path47.dirname(file), { recursive: true });
8990
+ await fs28.mkdir(path48.dirname(file), { recursive: true });
8845
8991
  const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
8846
8992
  const fd = await fs28.open(tmp, "w", 384);
8847
8993
  try {
@@ -8855,7 +9001,7 @@ async function writeConfigAtomically0600(file, contents) {
8855
9001
  }
8856
9002
  async function acquireConnectorsLock(lockPath, timeoutMs = CONNECTORS_LOCK_TIMEOUT_MS) {
8857
9003
  const deadline = Date.now() + timeoutMs;
8858
- await fs28.mkdir(path47.dirname(lockPath), { recursive: true });
9004
+ await fs28.mkdir(path48.dirname(lockPath), { recursive: true });
8859
9005
  for (; ; ) {
8860
9006
  try {
8861
9007
  const fd = await fs28.open(lockPath, "wx");
@@ -9849,4 +9995,4 @@ export {
9849
9995
  recordConnectorPoll,
9850
9996
  buildApi
9851
9997
  };
9852
- //# sourceMappingURL=chunk-7C6ZULNU.js.map
9998
+ //# sourceMappingURL=chunk-ZZ3VUCWL.js.map