@neat.is/core 0.3.3 → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-VABXPLDT.js → chunk-BUB3ASD5.js} +268 -142
- package/dist/chunk-BUB3ASD5.js.map +1 -0
- package/dist/{chunk-ICFH326H.js → chunk-KCEZSFU2.js} +2 -2
- package/dist/cli.cjs +309 -204
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +244 -144
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +251 -151
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +213 -115
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-VABXPLDT.js.map +0 -1
- /package/dist/{chunk-ICFH326H.js.map → chunk-KCEZSFU2.js.map} +0 -0
package/dist/neatd.cjs
CHANGED
|
@@ -100,8 +100,8 @@ function reshapeGrpcRequest(req2) {
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
function resolveProtoRoot() {
|
|
103
|
-
const here =
|
|
104
|
-
return
|
|
103
|
+
const here = import_node_path34.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
104
|
+
return import_node_path34.default.resolve(here, "..", "proto");
|
|
105
105
|
}
|
|
106
106
|
function loadTraceService() {
|
|
107
107
|
const protoRoot = resolveProtoRoot();
|
|
@@ -156,13 +156,13 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
156
156
|
})
|
|
157
157
|
};
|
|
158
158
|
}
|
|
159
|
-
var import_node_url,
|
|
159
|
+
var import_node_url, import_node_path34, grpc, protoLoader;
|
|
160
160
|
var init_otel_grpc = __esm({
|
|
161
161
|
"src/otel-grpc.ts"() {
|
|
162
162
|
"use strict";
|
|
163
163
|
init_cjs_shims();
|
|
164
164
|
import_node_url = require("url");
|
|
165
|
-
|
|
165
|
+
import_node_path34 = __toESM(require("path"), 1);
|
|
166
166
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
167
167
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
168
168
|
init_otel();
|
|
@@ -259,10 +259,10 @@ function parseOtlpRequest(body) {
|
|
|
259
259
|
}
|
|
260
260
|
function loadProtobufDecoder() {
|
|
261
261
|
if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
|
|
262
|
-
const here =
|
|
263
|
-
const protoRoot =
|
|
262
|
+
const here = import_node_path35.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
263
|
+
const protoRoot = import_node_path35.default.resolve(here, "..", "proto");
|
|
264
264
|
const root = new import_protobufjs.default.Root();
|
|
265
|
-
root.resolvePath = (_origin, target) =>
|
|
265
|
+
root.resolvePath = (_origin, target) => import_node_path35.default.resolve(protoRoot, target);
|
|
266
266
|
root.loadSync(
|
|
267
267
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
268
268
|
{ keepCase: true }
|
|
@@ -354,12 +354,12 @@ async function buildOtelReceiver(opts) {
|
|
|
354
354
|
};
|
|
355
355
|
return decorated;
|
|
356
356
|
}
|
|
357
|
-
var
|
|
357
|
+
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
358
358
|
var init_otel = __esm({
|
|
359
359
|
"src/otel.ts"() {
|
|
360
360
|
"use strict";
|
|
361
361
|
init_cjs_shims();
|
|
362
|
-
|
|
362
|
+
import_node_path35 = __toESM(require("path"), 1);
|
|
363
363
|
import_node_url2 = require("url");
|
|
364
364
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
365
365
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
@@ -370,12 +370,12 @@ var init_otel = __esm({
|
|
|
370
370
|
// src/neatd.ts
|
|
371
371
|
init_cjs_shims();
|
|
372
372
|
var import_node_fs22 = require("fs");
|
|
373
|
-
var
|
|
373
|
+
var import_node_path38 = __toESM(require("path"), 1);
|
|
374
374
|
|
|
375
375
|
// src/daemon.ts
|
|
376
376
|
init_cjs_shims();
|
|
377
377
|
var import_node_fs21 = require("fs");
|
|
378
|
-
var
|
|
378
|
+
var import_node_path36 = __toESM(require("path"), 1);
|
|
379
379
|
|
|
380
380
|
// src/graph.ts
|
|
381
381
|
init_cjs_shims();
|
|
@@ -749,11 +749,16 @@ init_cjs_shims();
|
|
|
749
749
|
var import_types = require("@neat.is/types");
|
|
750
750
|
var ROOT_CAUSE_MAX_DEPTH = 5;
|
|
751
751
|
var BLAST_RADIUS_DEFAULT_DEPTH = 10;
|
|
752
|
+
function isFrontierNode(graph, nodeId) {
|
|
753
|
+
if (!graph.hasNode(nodeId)) return false;
|
|
754
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
755
|
+
return attrs.type === import_types.NodeType.FrontierNode;
|
|
756
|
+
}
|
|
752
757
|
function bestEdgeBySource(graph, edgeIds) {
|
|
753
758
|
const best = /* @__PURE__ */ new Map();
|
|
754
759
|
for (const id of edgeIds) {
|
|
755
760
|
const e = graph.getEdgeAttributes(id);
|
|
756
|
-
if (e.
|
|
761
|
+
if (isFrontierNode(graph, e.source)) continue;
|
|
757
762
|
const cur = best.get(e.source);
|
|
758
763
|
if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
|
|
759
764
|
best.set(e.source, e);
|
|
@@ -765,7 +770,7 @@ function bestEdgeByTarget(graph, edgeIds) {
|
|
|
765
770
|
const best = /* @__PURE__ */ new Map();
|
|
766
771
|
for (const id of edgeIds) {
|
|
767
772
|
const e = graph.getEdgeAttributes(id);
|
|
768
|
-
if (e.
|
|
773
|
+
if (isFrontierNode(graph, e.target)) continue;
|
|
769
774
|
const cur = best.get(e.target);
|
|
770
775
|
if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
|
|
771
776
|
best.set(e.target, e);
|
|
@@ -777,8 +782,7 @@ var PROVENANCE_CEILING = {
|
|
|
777
782
|
OBSERVED: 1,
|
|
778
783
|
INFERRED: 0.7,
|
|
779
784
|
EXTRACTED: 0.5,
|
|
780
|
-
STALE: 0.3
|
|
781
|
-
FRONTIER: 0.3
|
|
785
|
+
STALE: 0.3
|
|
782
786
|
};
|
|
783
787
|
function volumeWeight(spanCount) {
|
|
784
788
|
if (!spanCount || spanCount <= 0) return 0.5;
|
|
@@ -831,19 +835,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
831
835
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
832
836
|
let best = { path: [start], edges: [] };
|
|
833
837
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
834
|
-
function step(node,
|
|
835
|
-
if (
|
|
836
|
-
best = { path: [...
|
|
838
|
+
function step(node, path39, edges) {
|
|
839
|
+
if (path39.length > best.path.length) {
|
|
840
|
+
best = { path: [...path39], edges: [...edges] };
|
|
837
841
|
}
|
|
838
|
-
if (
|
|
842
|
+
if (path39.length - 1 >= maxDepth) return;
|
|
839
843
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
840
844
|
for (const [srcId, edge] of incoming) {
|
|
841
845
|
if (visited.has(srcId)) continue;
|
|
842
846
|
visited.add(srcId);
|
|
843
|
-
|
|
847
|
+
path39.push(srcId);
|
|
844
848
|
edges.push(edge);
|
|
845
|
-
step(srcId,
|
|
846
|
-
|
|
849
|
+
step(srcId, path39, edges);
|
|
850
|
+
path39.pop();
|
|
847
851
|
edges.pop();
|
|
848
852
|
visited.delete(srcId);
|
|
849
853
|
}
|
|
@@ -1062,8 +1066,8 @@ var evaluateStructural = ({
|
|
|
1062
1066
|
for (const edgeId of graph.outboundEdges(id)) {
|
|
1063
1067
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1064
1068
|
if (e.type !== rule.edgeType) continue;
|
|
1065
|
-
if (e.provenance === import_types2.Provenance.FRONTIER) continue;
|
|
1066
1069
|
const target = graph.getNodeAttributes(e.target);
|
|
1070
|
+
if (target.type === import_types2.NodeType.FrontierNode) continue;
|
|
1067
1071
|
if (target.type === rule.toNodeType) {
|
|
1068
1072
|
satisfied = true;
|
|
1069
1073
|
break;
|
|
@@ -1182,8 +1186,8 @@ var evaluateCompatibility = ({
|
|
|
1182
1186
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
1183
1187
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1184
1188
|
if (e.type !== import_types2.EdgeType.CONNECTS_TO) continue;
|
|
1185
|
-
if (e.provenance === import_types2.Provenance.FRONTIER) continue;
|
|
1186
1189
|
const dbAttrs = graph.getNodeAttributes(e.target);
|
|
1190
|
+
if (dbAttrs.type === import_types2.NodeType.FrontierNode) continue;
|
|
1187
1191
|
if (dbAttrs.type !== import_types2.NodeType.DatabaseNode) continue;
|
|
1188
1192
|
const db = dbAttrs;
|
|
1189
1193
|
for (const pair of compatPairs()) {
|
|
@@ -1478,31 +1482,6 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
1478
1482
|
graph.addNode(id, node);
|
|
1479
1483
|
return id;
|
|
1480
1484
|
}
|
|
1481
|
-
function upsertFrontierEdge(graph, type, source, target, ts) {
|
|
1482
|
-
const id = (0, import_types3.frontierEdgeId)(source, target, type);
|
|
1483
|
-
if (graph.hasEdge(id)) {
|
|
1484
|
-
const existing = graph.getEdgeAttributes(id);
|
|
1485
|
-
const updated = {
|
|
1486
|
-
...existing,
|
|
1487
|
-
provenance: import_types3.Provenance.FRONTIER,
|
|
1488
|
-
lastObserved: ts,
|
|
1489
|
-
callCount: (existing.callCount ?? 0) + 1
|
|
1490
|
-
};
|
|
1491
|
-
graph.replaceEdgeAttributes(id, updated);
|
|
1492
|
-
return;
|
|
1493
|
-
}
|
|
1494
|
-
const edge = {
|
|
1495
|
-
id,
|
|
1496
|
-
source,
|
|
1497
|
-
target,
|
|
1498
|
-
type,
|
|
1499
|
-
provenance: import_types3.Provenance.FRONTIER,
|
|
1500
|
-
confidence: 1,
|
|
1501
|
-
lastObserved: ts,
|
|
1502
|
-
callCount: 1
|
|
1503
|
-
};
|
|
1504
|
-
graph.addEdgeWithKey(id, source, target, edge);
|
|
1505
|
-
}
|
|
1506
1485
|
function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
1507
1486
|
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
1508
1487
|
const id = makeObservedEdgeId(type, source, target);
|
|
@@ -1510,35 +1489,37 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
|
1510
1489
|
const existing = graph.getEdgeAttributes(id);
|
|
1511
1490
|
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
1512
1491
|
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
1492
|
+
const newSignal = {
|
|
1493
|
+
spanCount: newSpanCount,
|
|
1494
|
+
errorCount: newErrorCount,
|
|
1495
|
+
lastObservedAgeMs: 0
|
|
1496
|
+
};
|
|
1513
1497
|
const updated = {
|
|
1514
1498
|
...existing,
|
|
1515
1499
|
provenance: import_types3.Provenance.OBSERVED,
|
|
1516
1500
|
lastObserved: ts,
|
|
1517
1501
|
callCount: newSpanCount,
|
|
1518
|
-
signal:
|
|
1519
|
-
|
|
1520
|
-
errorCount: newErrorCount,
|
|
1521
|
-
lastObservedAgeMs: 0
|
|
1522
|
-
},
|
|
1523
|
-
confidence: 1
|
|
1502
|
+
signal: newSignal,
|
|
1503
|
+
confidence: (0, import_types3.confidenceForObservedSignal)(newSignal)
|
|
1524
1504
|
};
|
|
1525
1505
|
graph.replaceEdgeAttributes(id, updated);
|
|
1526
1506
|
return { edge: updated, created: false };
|
|
1527
1507
|
}
|
|
1508
|
+
const signal = {
|
|
1509
|
+
spanCount: 1,
|
|
1510
|
+
errorCount: isError ? 1 : 0,
|
|
1511
|
+
lastObservedAgeMs: 0
|
|
1512
|
+
};
|
|
1528
1513
|
const edge = {
|
|
1529
1514
|
id,
|
|
1530
1515
|
source,
|
|
1531
1516
|
target,
|
|
1532
1517
|
type,
|
|
1533
1518
|
provenance: import_types3.Provenance.OBSERVED,
|
|
1534
|
-
confidence:
|
|
1519
|
+
confidence: (0, import_types3.confidenceForObservedSignal)(signal),
|
|
1535
1520
|
lastObserved: ts,
|
|
1536
1521
|
callCount: 1,
|
|
1537
|
-
signal
|
|
1538
|
-
spanCount: 1,
|
|
1539
|
-
errorCount: isError ? 1 : 0,
|
|
1540
|
-
lastObservedAgeMs: 0
|
|
1541
|
-
}
|
|
1522
|
+
signal
|
|
1542
1523
|
};
|
|
1543
1524
|
graph.addEdgeWithKey(id, source, target, edge);
|
|
1544
1525
|
return { edge, created: true };
|
|
@@ -1586,18 +1567,28 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
1586
1567
|
await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(ctx.errorsPath), { recursive: true });
|
|
1587
1568
|
await import_node_fs3.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
1588
1569
|
}
|
|
1570
|
+
function sanitizeAttributes(attrs) {
|
|
1571
|
+
const out = {};
|
|
1572
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
1573
|
+
if (typeof v === "bigint") out[k] = v.toString();
|
|
1574
|
+
else out[k] = v;
|
|
1575
|
+
}
|
|
1576
|
+
return out;
|
|
1577
|
+
}
|
|
1589
1578
|
function buildErrorEventForReceiver(span) {
|
|
1590
1579
|
if (span.statusCode !== 2) return null;
|
|
1591
1580
|
const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1581
|
+
const attrs = sanitizeAttributes(span.attributes);
|
|
1592
1582
|
return {
|
|
1593
1583
|
id: `${span.traceId}:${span.spanId}`,
|
|
1594
1584
|
timestamp: ts,
|
|
1595
1585
|
service: span.service,
|
|
1596
1586
|
traceId: span.traceId,
|
|
1597
1587
|
spanId: span.spanId,
|
|
1598
|
-
errorMessage: span.exception?.message ?? span.
|
|
1588
|
+
errorMessage: span.exception?.message ?? span.name ?? "unknown error",
|
|
1599
1589
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1600
1590
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1591
|
+
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
1601
1592
|
affectedNode: (0, import_types3.serviceId)(span.service)
|
|
1602
1593
|
};
|
|
1603
1594
|
}
|
|
@@ -1648,11 +1639,16 @@ async function handleSpan(ctx, span) {
|
|
|
1648
1639
|
affectedNode = targetId;
|
|
1649
1640
|
resolvedViaAddress = true;
|
|
1650
1641
|
} else if (!targetId) {
|
|
1651
|
-
const
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1642
|
+
const frontierNodeId = ensureFrontierNode(ctx.graph, host, ts);
|
|
1643
|
+
upsertObservedEdge(
|
|
1644
|
+
ctx.graph,
|
|
1645
|
+
import_types3.EdgeType.CALLS,
|
|
1646
|
+
sourceId,
|
|
1647
|
+
frontierNodeId,
|
|
1648
|
+
ts,
|
|
1649
|
+
isError
|
|
1650
|
+
);
|
|
1651
|
+
affectedNode = frontierNodeId;
|
|
1656
1652
|
resolvedViaAddress = true;
|
|
1657
1653
|
}
|
|
1658
1654
|
}
|
|
@@ -1674,15 +1670,17 @@ async function handleSpan(ctx, span) {
|
|
|
1674
1670
|
if (span.statusCode === 2) {
|
|
1675
1671
|
stitchTrace(ctx.graph, sourceId, ts);
|
|
1676
1672
|
if (ctx.writeErrorEventInline !== false) {
|
|
1673
|
+
const attrs = sanitizeAttributes(span.attributes);
|
|
1677
1674
|
const ev = {
|
|
1678
1675
|
id: `${span.traceId}:${span.spanId}`,
|
|
1679
1676
|
timestamp: ts,
|
|
1680
1677
|
service: span.service,
|
|
1681
1678
|
traceId: span.traceId,
|
|
1682
1679
|
spanId: span.spanId,
|
|
1683
|
-
errorMessage: span.exception?.message ?? span.
|
|
1680
|
+
errorMessage: span.exception?.message ?? span.name ?? "unknown error",
|
|
1684
1681
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1685
1682
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1683
|
+
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
1686
1684
|
affectedNode
|
|
1687
1685
|
};
|
|
1688
1686
|
await appendErrorEvent(ctx, ev);
|
|
@@ -1738,8 +1736,7 @@ function rewireFrontierEdges(graph, frontierId2, serviceId3) {
|
|
|
1738
1736
|
}
|
|
1739
1737
|
function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
1740
1738
|
graph.dropEdge(oldEdgeId);
|
|
1741
|
-
const
|
|
1742
|
-
const newId = promotedProvenance === import_types3.Provenance.OBSERVED ? (0, import_types3.observedEdgeId)(newSource, newTarget, edge.type) : promotedProvenance === import_types3.Provenance.INFERRED ? (0, import_types3.inferredEdgeId)(newSource, newTarget, edge.type) : promotedProvenance === import_types3.Provenance.EXTRACTED ? (0, import_types3.extractedEdgeId)(newSource, newTarget, edge.type) : (0, import_types3.frontierEdgeId)(newSource, newTarget, edge.type);
|
|
1739
|
+
const newId = edge.provenance === import_types3.Provenance.OBSERVED ? (0, import_types3.observedEdgeId)(newSource, newTarget, edge.type) : edge.provenance === import_types3.Provenance.INFERRED ? (0, import_types3.inferredEdgeId)(newSource, newTarget, edge.type) : (0, import_types3.extractedEdgeId)(newSource, newTarget, edge.type);
|
|
1743
1740
|
if (graph.hasEdge(newId)) {
|
|
1744
1741
|
const existing = graph.getEdgeAttributes(newId);
|
|
1745
1742
|
const merged = {
|
|
@@ -1754,8 +1751,7 @@ function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
|
1754
1751
|
...edge,
|
|
1755
1752
|
id: newId,
|
|
1756
1753
|
source: newSource,
|
|
1757
|
-
target: newTarget
|
|
1758
|
-
provenance: promotedProvenance
|
|
1754
|
+
target: newTarget
|
|
1759
1755
|
};
|
|
1760
1756
|
graph.addEdgeWithKey(newId, newSource, newTarget, rebuilt);
|
|
1761
1757
|
}
|
|
@@ -2092,6 +2088,23 @@ async function writeExtractionErrors(errors, errorsPath) {
|
|
|
2092
2088
|
const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2093
2089
|
await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
|
|
2094
2090
|
}
|
|
2091
|
+
var droppedSink = [];
|
|
2092
|
+
function noteExtractedDropped(edge) {
|
|
2093
|
+
droppedSink.push(edge);
|
|
2094
|
+
}
|
|
2095
|
+
function drainDroppedExtracted() {
|
|
2096
|
+
return droppedSink.splice(0, droppedSink.length);
|
|
2097
|
+
}
|
|
2098
|
+
function isRejectedLogEnabled() {
|
|
2099
|
+
const raw = process.env.NEAT_EXTRACTED_REJECTED_LOG;
|
|
2100
|
+
return raw === "1" || raw === "true";
|
|
2101
|
+
}
|
|
2102
|
+
async function writeRejectedExtracted(drops, rejectedPath) {
|
|
2103
|
+
if (drops.length === 0) return;
|
|
2104
|
+
await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(rejectedPath), { recursive: true });
|
|
2105
|
+
const lines = drops.map((d) => JSON.stringify({ ...d, ts: (/* @__PURE__ */ new Date()).toISOString() })).join("\n") + "\n";
|
|
2106
|
+
await import_node_fs7.promises.appendFile(rejectedPath, lines, "utf8");
|
|
2107
|
+
}
|
|
2095
2108
|
|
|
2096
2109
|
// src/extract/services.ts
|
|
2097
2110
|
var DEFAULT_SCAN_DEPTH = 5;
|
|
@@ -3030,6 +3043,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3030
3043
|
target: dbNode.id,
|
|
3031
3044
|
type: import_types7.EdgeType.CONNECTS_TO,
|
|
3032
3045
|
provenance: import_types7.Provenance.EXTRACTED,
|
|
3046
|
+
confidence: (0, import_types7.confidenceForExtracted)("structural"),
|
|
3033
3047
|
// ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
|
|
3034
3048
|
// Ghost-edge cleanup keys retirement on this; the conditional
|
|
3035
3049
|
// sourceFile spread that used to live here was a v0.1.x leftover.
|
|
@@ -3104,6 +3118,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3104
3118
|
target: node.id,
|
|
3105
3119
|
type: import_types8.EdgeType.CONFIGURED_BY,
|
|
3106
3120
|
provenance: import_types8.Provenance.EXTRACTED,
|
|
3121
|
+
confidence: (0, import_types8.confidenceForExtracted)("structural"),
|
|
3107
3122
|
evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
|
|
3108
3123
|
};
|
|
3109
3124
|
if (!graph.hasEdge(edge.id)) {
|
|
@@ -3258,17 +3273,32 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3258
3273
|
for (const [targetId, evidenceFile] of seenTargets) {
|
|
3259
3274
|
const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
|
|
3260
3275
|
const line = lineOf(fileContent, `//${evidenceFile.host}`);
|
|
3276
|
+
const confidence = (0, import_types9.confidenceForExtracted)("hostname-shape-match");
|
|
3277
|
+
const ev = {
|
|
3278
|
+
file: import_node_path20.default.relative(service.dir, evidenceFile.file),
|
|
3279
|
+
line,
|
|
3280
|
+
snippet: snippet(fileContent, line)
|
|
3281
|
+
};
|
|
3282
|
+
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS);
|
|
3283
|
+
if (!(0, import_types9.passesExtractedFloor)(confidence)) {
|
|
3284
|
+
noteExtractedDropped({
|
|
3285
|
+
source: service.node.id,
|
|
3286
|
+
target: targetId,
|
|
3287
|
+
type: import_types9.EdgeType.CALLS,
|
|
3288
|
+
confidence,
|
|
3289
|
+
confidenceKind: "hostname-shape-match",
|
|
3290
|
+
evidence: ev
|
|
3291
|
+
});
|
|
3292
|
+
continue;
|
|
3293
|
+
}
|
|
3261
3294
|
const edge = {
|
|
3262
|
-
id:
|
|
3295
|
+
id: edgeId,
|
|
3263
3296
|
source: service.node.id,
|
|
3264
3297
|
target: targetId,
|
|
3265
3298
|
type: import_types9.EdgeType.CALLS,
|
|
3266
3299
|
provenance: import_types9.Provenance.EXTRACTED,
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
line,
|
|
3270
|
-
snippet: snippet(fileContent, line)
|
|
3271
|
-
}
|
|
3300
|
+
confidence,
|
|
3301
|
+
evidence: ev
|
|
3272
3302
|
};
|
|
3273
3303
|
if (!graph.hasEdge(edge.id)) {
|
|
3274
3304
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3307,6 +3337,10 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3307
3337
|
name: topic,
|
|
3308
3338
|
kind: "kafka-topic",
|
|
3309
3339
|
edgeType,
|
|
3340
|
+
// `producer.send({topic: 'x'})` / `consumer.subscribe({topic: 'x'})` —
|
|
3341
|
+
// framework-aware (kafkajs / node-rdkafka shape). Verified-call-site
|
|
3342
|
+
// tier (ADR-066).
|
|
3343
|
+
confidenceKind: "verified-call-site",
|
|
3310
3344
|
evidence: {
|
|
3311
3345
|
file: import_node_path21.default.relative(serviceDir, file.path),
|
|
3312
3346
|
line,
|
|
@@ -3339,6 +3373,10 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3339
3373
|
name: host,
|
|
3340
3374
|
kind: "redis",
|
|
3341
3375
|
edgeType: "CALLS",
|
|
3376
|
+
// `redis://host` URL literal — the scheme is structural support, but no
|
|
3377
|
+
// call expression is verified to wire it through. URL-with-structural-
|
|
3378
|
+
// support tier (ADR-066).
|
|
3379
|
+
confidenceKind: "url-with-structural-support",
|
|
3342
3380
|
evidence: {
|
|
3343
3381
|
file: import_node_path22.default.relative(serviceDir, file.path),
|
|
3344
3382
|
line,
|
|
@@ -3380,6 +3418,10 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3380
3418
|
name,
|
|
3381
3419
|
kind,
|
|
3382
3420
|
edgeType: "CALLS",
|
|
3421
|
+
// SDK marker (S3Client, GetCommand, etc.) plus a Bucket/TableName
|
|
3422
|
+
// literal — framework-aware recognizer, verified-call-site tier
|
|
3423
|
+
// (ADR-066).
|
|
3424
|
+
confidenceKind: "verified-call-site",
|
|
3383
3425
|
evidence: {
|
|
3384
3426
|
file: import_node_path23.default.relative(serviceDir, file.path),
|
|
3385
3427
|
line,
|
|
@@ -3459,6 +3501,10 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3459
3501
|
name,
|
|
3460
3502
|
kind,
|
|
3461
3503
|
edgeType: "CALLS",
|
|
3504
|
+
// `new <Name>Client(...)` with @aws-sdk/* or @grpc/grpc-js import
|
|
3505
|
+
// context — import-aware classification per #238. Verified-call-site
|
|
3506
|
+
// tier (ADR-066).
|
|
3507
|
+
confidenceKind: "verified-call-site",
|
|
3462
3508
|
evidence: {
|
|
3463
3509
|
file: import_node_path24.default.relative(serviceDir, file.path),
|
|
3464
3510
|
line,
|
|
@@ -3519,6 +3565,18 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3519
3565
|
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
|
|
3520
3566
|
if (seenEdges.has(edgeId)) continue;
|
|
3521
3567
|
seenEdges.add(edgeId);
|
|
3568
|
+
const confidence = (0, import_types14.confidenceForExtracted)(ep.confidenceKind);
|
|
3569
|
+
if (!(0, import_types14.passesExtractedFloor)(confidence)) {
|
|
3570
|
+
noteExtractedDropped({
|
|
3571
|
+
source: service.node.id,
|
|
3572
|
+
target: ep.infraId,
|
|
3573
|
+
type: edgeType,
|
|
3574
|
+
confidence,
|
|
3575
|
+
confidenceKind: ep.confidenceKind,
|
|
3576
|
+
evidence: ep.evidence
|
|
3577
|
+
});
|
|
3578
|
+
continue;
|
|
3579
|
+
}
|
|
3522
3580
|
if (!graph.hasEdge(edgeId)) {
|
|
3523
3581
|
const edge = {
|
|
3524
3582
|
id: edgeId,
|
|
@@ -3526,6 +3584,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3526
3584
|
target: ep.infraId,
|
|
3527
3585
|
type: edgeType,
|
|
3528
3586
|
provenance: import_types14.Provenance.EXTRACTED,
|
|
3587
|
+
confidence,
|
|
3529
3588
|
evidence: ep.evidence
|
|
3530
3589
|
};
|
|
3531
3590
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3645,6 +3704,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3645
3704
|
target: targetId,
|
|
3646
3705
|
type: import_types16.EdgeType.DEPENDS_ON,
|
|
3647
3706
|
provenance: import_types16.Provenance.EXTRACTED,
|
|
3707
|
+
confidence: (0, import_types16.confidenceForExtracted)("structural"),
|
|
3648
3708
|
evidence: { file: evidenceFile }
|
|
3649
3709
|
};
|
|
3650
3710
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3705,6 +3765,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3705
3765
|
target: node.id,
|
|
3706
3766
|
type: import_types17.EdgeType.RUNS_ON,
|
|
3707
3767
|
provenance: import_types17.Provenance.EXTRACTED,
|
|
3768
|
+
confidence: (0, import_types17.confidenceForExtracted)("structural"),
|
|
3708
3769
|
evidence: {
|
|
3709
3770
|
file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
|
|
3710
3771
|
}
|
|
@@ -3821,6 +3882,9 @@ async function addInfra(graph, scanPath, services) {
|
|
|
3821
3882
|
};
|
|
3822
3883
|
}
|
|
3823
3884
|
|
|
3885
|
+
// src/extract/index.ts
|
|
3886
|
+
var import_node_path30 = __toESM(require("path"), 1);
|
|
3887
|
+
|
|
3824
3888
|
// src/extract/retire.ts
|
|
3825
3889
|
init_cjs_shims();
|
|
3826
3890
|
var import_node_fs17 = require("fs");
|
|
@@ -3873,13 +3937,26 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3873
3937
|
);
|
|
3874
3938
|
}
|
|
3875
3939
|
}
|
|
3940
|
+
const droppedEntries = drainDroppedExtracted();
|
|
3941
|
+
if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
|
|
3942
|
+
const rejectedPath = import_node_path30.default.join(import_node_path30.default.dirname(opts.errorsPath), "rejected.ndjson");
|
|
3943
|
+
try {
|
|
3944
|
+
await writeRejectedExtracted(droppedEntries, rejectedPath);
|
|
3945
|
+
} catch (err) {
|
|
3946
|
+
console.warn(
|
|
3947
|
+
`[neat] failed to write rejected extracted edges to ${rejectedPath}: ${err.message}`
|
|
3948
|
+
);
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3876
3951
|
const result = {
|
|
3877
3952
|
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
3878
3953
|
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
3879
3954
|
frontiersPromoted,
|
|
3880
3955
|
extractionErrors: errorEntries.length,
|
|
3881
3956
|
errorEntries,
|
|
3882
|
-
ghostsRetired
|
|
3957
|
+
ghostsRetired,
|
|
3958
|
+
extractedDropped: droppedEntries.length,
|
|
3959
|
+
droppedEntries
|
|
3883
3960
|
};
|
|
3884
3961
|
emitNeatEvent({
|
|
3885
3962
|
type: "extraction-complete",
|
|
@@ -3897,8 +3974,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3897
3974
|
// src/persist.ts
|
|
3898
3975
|
init_cjs_shims();
|
|
3899
3976
|
var import_node_fs18 = require("fs");
|
|
3900
|
-
var
|
|
3901
|
-
var
|
|
3977
|
+
var import_node_path31 = __toESM(require("path"), 1);
|
|
3978
|
+
var import_types19 = require("@neat.is/types");
|
|
3979
|
+
var SCHEMA_VERSION = 3;
|
|
3902
3980
|
function migrateV1ToV2(payload) {
|
|
3903
3981
|
const nodes = payload.graph.nodes;
|
|
3904
3982
|
if (Array.isArray(nodes)) {
|
|
@@ -3910,8 +3988,27 @@ function migrateV1ToV2(payload) {
|
|
|
3910
3988
|
}
|
|
3911
3989
|
return { ...payload, schemaVersion: 2 };
|
|
3912
3990
|
}
|
|
3991
|
+
function migrateV2ToV3(payload) {
|
|
3992
|
+
const edges = payload.graph.edges;
|
|
3993
|
+
if (Array.isArray(edges)) {
|
|
3994
|
+
for (const edge of edges) {
|
|
3995
|
+
const attrs = edge.attributes;
|
|
3996
|
+
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
3997
|
+
attrs.provenance = import_types19.Provenance.OBSERVED;
|
|
3998
|
+
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
3999
|
+
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
4000
|
+
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
4001
|
+
if (type && source && target) {
|
|
4002
|
+
const newId = (0, import_types19.observedEdgeId)(source, target, type);
|
|
4003
|
+
attrs.id = newId;
|
|
4004
|
+
if (edge.key) edge.key = newId;
|
|
4005
|
+
}
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
return { ...payload, schemaVersion: 3 };
|
|
4009
|
+
}
|
|
3913
4010
|
async function ensureDir(filePath) {
|
|
3914
|
-
await import_node_fs18.promises.mkdir(
|
|
4011
|
+
await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
|
|
3915
4012
|
}
|
|
3916
4013
|
async function saveGraphToDisk(graph, outPath) {
|
|
3917
4014
|
await ensureDir(outPath);
|
|
@@ -3936,6 +4033,9 @@ async function loadGraphFromDisk(graph, outPath) {
|
|
|
3936
4033
|
if (payload.schemaVersion === 1) {
|
|
3937
4034
|
payload = migrateV1ToV2(payload);
|
|
3938
4035
|
}
|
|
4036
|
+
if (payload.schemaVersion === 2) {
|
|
4037
|
+
payload = migrateV2ToV3(payload);
|
|
4038
|
+
}
|
|
3939
4039
|
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
3940
4040
|
throw new Error(
|
|
3941
4041
|
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
@@ -3980,23 +4080,23 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
3980
4080
|
|
|
3981
4081
|
// src/projects.ts
|
|
3982
4082
|
init_cjs_shims();
|
|
3983
|
-
var
|
|
4083
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
3984
4084
|
function pathsForProject(project, baseDir) {
|
|
3985
4085
|
if (project === DEFAULT_PROJECT) {
|
|
3986
4086
|
return {
|
|
3987
|
-
snapshotPath:
|
|
3988
|
-
errorsPath:
|
|
3989
|
-
staleEventsPath:
|
|
3990
|
-
embeddingsCachePath:
|
|
3991
|
-
policyViolationsPath:
|
|
4087
|
+
snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
|
|
4088
|
+
errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
|
|
4089
|
+
staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
|
|
4090
|
+
embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
|
|
4091
|
+
policyViolationsPath: import_node_path32.default.join(baseDir, "policy-violations.ndjson")
|
|
3992
4092
|
};
|
|
3993
4093
|
}
|
|
3994
4094
|
return {
|
|
3995
|
-
snapshotPath:
|
|
3996
|
-
errorsPath:
|
|
3997
|
-
staleEventsPath:
|
|
3998
|
-
embeddingsCachePath:
|
|
3999
|
-
policyViolationsPath:
|
|
4095
|
+
snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
|
|
4096
|
+
errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
|
|
4097
|
+
staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
4098
|
+
embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
|
|
4099
|
+
policyViolationsPath: import_node_path32.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
4000
4100
|
};
|
|
4001
4101
|
}
|
|
4002
4102
|
var Projects = class {
|
|
@@ -4034,11 +4134,11 @@ var Projects = class {
|
|
|
4034
4134
|
init_cjs_shims();
|
|
4035
4135
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
4036
4136
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
4037
|
-
var
|
|
4137
|
+
var import_types22 = require("@neat.is/types");
|
|
4038
4138
|
|
|
4039
4139
|
// src/divergences.ts
|
|
4040
4140
|
init_cjs_shims();
|
|
4041
|
-
var
|
|
4141
|
+
var import_types20 = require("@neat.is/types");
|
|
4042
4142
|
function bucketKey(source, target, type) {
|
|
4043
4143
|
return `${type}|${source}|${target}`;
|
|
4044
4144
|
}
|
|
@@ -4046,25 +4146,22 @@ function bucketEdges(graph) {
|
|
|
4046
4146
|
const buckets = /* @__PURE__ */ new Map();
|
|
4047
4147
|
graph.forEachEdge((id, attrs) => {
|
|
4048
4148
|
const e = attrs;
|
|
4049
|
-
const parsed = (0,
|
|
4149
|
+
const parsed = (0, import_types20.parseEdgeId)(id);
|
|
4050
4150
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
4051
4151
|
const key = bucketKey(e.source, e.target, e.type);
|
|
4052
4152
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
4053
4153
|
switch (provenance) {
|
|
4054
|
-
case
|
|
4154
|
+
case import_types20.Provenance.EXTRACTED:
|
|
4055
4155
|
cur.extracted = e;
|
|
4056
4156
|
break;
|
|
4057
|
-
case
|
|
4157
|
+
case import_types20.Provenance.OBSERVED:
|
|
4058
4158
|
cur.observed = e;
|
|
4059
4159
|
break;
|
|
4060
|
-
case
|
|
4160
|
+
case import_types20.Provenance.INFERRED:
|
|
4061
4161
|
cur.inferred = e;
|
|
4062
4162
|
break;
|
|
4063
|
-
case import_types19.Provenance.FRONTIER:
|
|
4064
|
-
cur.frontier = e;
|
|
4065
|
-
break;
|
|
4066
4163
|
default:
|
|
4067
|
-
if (e.provenance ===
|
|
4164
|
+
if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
|
|
4068
4165
|
}
|
|
4069
4166
|
buckets.set(key, cur);
|
|
4070
4167
|
});
|
|
@@ -4073,15 +4170,7 @@ function bucketEdges(graph) {
|
|
|
4073
4170
|
function nodeIsFrontier(graph, nodeId) {
|
|
4074
4171
|
if (!graph.hasNode(nodeId)) return false;
|
|
4075
4172
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
4076
|
-
return attrs.type ===
|
|
4077
|
-
}
|
|
4078
|
-
function hasAnyObservedFromSource(graph, sourceId) {
|
|
4079
|
-
if (!graph.hasNode(sourceId)) return false;
|
|
4080
|
-
for (const edgeId of graph.outboundEdges(sourceId)) {
|
|
4081
|
-
const e = graph.getEdgeAttributes(edgeId);
|
|
4082
|
-
if (e.provenance === import_types19.Provenance.OBSERVED) return true;
|
|
4083
|
-
}
|
|
4084
|
-
return false;
|
|
4173
|
+
return attrs.type === import_types20.NodeType.FrontierNode;
|
|
4085
4174
|
}
|
|
4086
4175
|
function clampConfidence(n) {
|
|
4087
4176
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -4096,32 +4185,34 @@ function reasonForMissingExtracted(source, target, type) {
|
|
|
4096
4185
|
var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
|
|
4097
4186
|
var RECOMMENDATION_MISSING_EXTRACTED = "Likely dynamic dispatch, reflection, or a coverage gap in tree-sitter extraction. Consider an `aliases` entry on the source service or file an extractor issue.";
|
|
4098
4187
|
var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
|
|
4188
|
+
function gradedConfidence(edge) {
|
|
4189
|
+
if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
|
|
4190
|
+
return clampConfidence(confidenceForEdge(edge));
|
|
4191
|
+
}
|
|
4099
4192
|
function detectMissingDivergences(graph, bucket) {
|
|
4100
4193
|
const out = [];
|
|
4101
4194
|
if (bucket.extracted && !bucket.observed) {
|
|
4102
4195
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
4103
|
-
const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
|
|
4104
4196
|
out.push({
|
|
4105
4197
|
type: "missing-observed",
|
|
4106
4198
|
source: bucket.source,
|
|
4107
4199
|
target: bucket.target,
|
|
4108
4200
|
edgeType: bucket.type,
|
|
4109
4201
|
extracted: bucket.extracted,
|
|
4110
|
-
confidence:
|
|
4202
|
+
confidence: gradedConfidence(bucket.extracted),
|
|
4111
4203
|
reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
|
|
4112
4204
|
recommendation: RECOMMENDATION_MISSING_OBSERVED
|
|
4113
4205
|
});
|
|
4114
4206
|
}
|
|
4115
4207
|
}
|
|
4116
4208
|
if (bucket.observed && !bucket.extracted) {
|
|
4117
|
-
const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
|
|
4118
4209
|
out.push({
|
|
4119
4210
|
type: "missing-extracted",
|
|
4120
4211
|
source: bucket.source,
|
|
4121
4212
|
target: bucket.target,
|
|
4122
4213
|
edgeType: bucket.type,
|
|
4123
4214
|
observed: bucket.observed,
|
|
4124
|
-
confidence:
|
|
4215
|
+
confidence: gradedConfidence(bucket.observed),
|
|
4125
4216
|
reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
|
|
4126
4217
|
recommendation: RECOMMENDATION_MISSING_EXTRACTED
|
|
4127
4218
|
});
|
|
@@ -4140,7 +4231,7 @@ function declaredHostFor(svc) {
|
|
|
4140
4231
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
4141
4232
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4142
4233
|
const e = graph.getEdgeAttributes(edgeId);
|
|
4143
|
-
if (e.type ===
|
|
4234
|
+
if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
|
|
4144
4235
|
return true;
|
|
4145
4236
|
}
|
|
4146
4237
|
}
|
|
@@ -4153,10 +4244,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
4153
4244
|
const out = [];
|
|
4154
4245
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4155
4246
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4156
|
-
if (edge.type !==
|
|
4157
|
-
if (edge.provenance !==
|
|
4247
|
+
if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
|
|
4248
|
+
if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
|
|
4158
4249
|
const target = graph.getNodeAttributes(edge.target);
|
|
4159
|
-
if (target.type !==
|
|
4250
|
+
if (target.type !== import_types20.NodeType.DatabaseNode) continue;
|
|
4160
4251
|
const observedHost = target.host?.trim();
|
|
4161
4252
|
if (!observedHost) continue;
|
|
4162
4253
|
if (observedHost === declaredHost) continue;
|
|
@@ -4178,10 +4269,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
4178
4269
|
const deps = svc.dependencies ?? {};
|
|
4179
4270
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4180
4271
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4181
|
-
if (edge.type !==
|
|
4182
|
-
if (edge.provenance !==
|
|
4272
|
+
if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
|
|
4273
|
+
if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
|
|
4183
4274
|
const target = graph.getNodeAttributes(edge.target);
|
|
4184
|
-
if (target.type !==
|
|
4275
|
+
if (target.type !== import_types20.NodeType.DatabaseNode) continue;
|
|
4185
4276
|
for (const pair of compatPairs()) {
|
|
4186
4277
|
if (pair.engine !== target.engine) continue;
|
|
4187
4278
|
const declared = deps[pair.driver];
|
|
@@ -4242,7 +4333,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4242
4333
|
}
|
|
4243
4334
|
graph.forEachNode((nodeId, attrs) => {
|
|
4244
4335
|
const n = attrs;
|
|
4245
|
-
if (n.type !==
|
|
4336
|
+
if (n.type !== import_types20.NodeType.ServiceNode) return;
|
|
4246
4337
|
const svc = n;
|
|
4247
4338
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4248
4339
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
@@ -4260,13 +4351,22 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4260
4351
|
const target = opts.node;
|
|
4261
4352
|
filtered = filtered.filter((d) => involvesNode(d, target));
|
|
4262
4353
|
}
|
|
4354
|
+
const TYPE_LEADERSHIP = {
|
|
4355
|
+
"missing-extracted": 0,
|
|
4356
|
+
"missing-observed": 1,
|
|
4357
|
+
"version-mismatch": 2,
|
|
4358
|
+
"host-mismatch": 3,
|
|
4359
|
+
"compat-violation": 4
|
|
4360
|
+
};
|
|
4263
4361
|
filtered.sort((a, b) => {
|
|
4264
4362
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
4363
|
+
const lead = TYPE_LEADERSHIP[a.type] - TYPE_LEADERSHIP[b.type];
|
|
4364
|
+
if (lead !== 0) return lead;
|
|
4265
4365
|
if (a.type !== b.type) return a.type.localeCompare(b.type);
|
|
4266
4366
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4267
4367
|
return a.target.localeCompare(b.target);
|
|
4268
4368
|
});
|
|
4269
|
-
return
|
|
4369
|
+
return import_types20.DivergenceResultSchema.parse({
|
|
4270
4370
|
divergences: filtered,
|
|
4271
4371
|
totalAffected: filtered.length,
|
|
4272
4372
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -4354,23 +4454,23 @@ function canonicalJson(value) {
|
|
|
4354
4454
|
init_cjs_shims();
|
|
4355
4455
|
var import_node_fs20 = require("fs");
|
|
4356
4456
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4357
|
-
var
|
|
4358
|
-
var
|
|
4457
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
4458
|
+
var import_types21 = require("@neat.is/types");
|
|
4359
4459
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
4360
4460
|
var LOCK_RETRY_MS = 50;
|
|
4361
4461
|
function neatHome() {
|
|
4362
4462
|
const override = process.env.NEAT_HOME;
|
|
4363
|
-
if (override && override.length > 0) return
|
|
4364
|
-
return
|
|
4463
|
+
if (override && override.length > 0) return import_node_path33.default.resolve(override);
|
|
4464
|
+
return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
|
|
4365
4465
|
}
|
|
4366
4466
|
function registryPath() {
|
|
4367
|
-
return
|
|
4467
|
+
return import_node_path33.default.join(neatHome(), "projects.json");
|
|
4368
4468
|
}
|
|
4369
4469
|
function registryLockPath() {
|
|
4370
|
-
return
|
|
4470
|
+
return import_node_path33.default.join(neatHome(), "projects.json.lock");
|
|
4371
4471
|
}
|
|
4372
4472
|
async function writeAtomically(target, contents) {
|
|
4373
|
-
await import_node_fs20.promises.mkdir(
|
|
4473
|
+
await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
|
|
4374
4474
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4375
4475
|
const fd = await import_node_fs20.promises.open(tmp, "w");
|
|
4376
4476
|
try {
|
|
@@ -4383,7 +4483,7 @@ async function writeAtomically(target, contents) {
|
|
|
4383
4483
|
}
|
|
4384
4484
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4385
4485
|
const deadline = Date.now() + timeoutMs;
|
|
4386
|
-
await import_node_fs20.promises.mkdir(
|
|
4486
|
+
await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
|
|
4387
4487
|
while (true) {
|
|
4388
4488
|
try {
|
|
4389
4489
|
const fd = await import_node_fs20.promises.open(lockPath, "wx");
|
|
@@ -4426,10 +4526,10 @@ async function readRegistry() {
|
|
|
4426
4526
|
throw err;
|
|
4427
4527
|
}
|
|
4428
4528
|
const parsed = JSON.parse(raw);
|
|
4429
|
-
return
|
|
4529
|
+
return import_types21.RegistryFileSchema.parse(parsed);
|
|
4430
4530
|
}
|
|
4431
4531
|
async function writeRegistry(reg) {
|
|
4432
|
-
const validated =
|
|
4532
|
+
const validated = import_types21.RegistryFileSchema.parse(reg);
|
|
4433
4533
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4434
4534
|
}
|
|
4435
4535
|
async function getProject(name) {
|
|
@@ -4639,11 +4739,11 @@ function registerRoutes(scope, ctx) {
|
|
|
4639
4739
|
const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4640
4740
|
const parsed = [];
|
|
4641
4741
|
for (const c of candidates) {
|
|
4642
|
-
const r =
|
|
4742
|
+
const r = import_types22.DivergenceTypeSchema.safeParse(c);
|
|
4643
4743
|
if (!r.success) {
|
|
4644
4744
|
return reply.code(400).send({
|
|
4645
4745
|
error: `unknown divergence type "${c}"`,
|
|
4646
|
-
allowed:
|
|
4746
|
+
allowed: import_types22.DivergenceTypeSchema.options
|
|
4647
4747
|
});
|
|
4648
4748
|
}
|
|
4649
4749
|
parsed.push(r.data);
|
|
@@ -4827,7 +4927,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4827
4927
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4828
4928
|
let violations = await log.readAll();
|
|
4829
4929
|
if (req2.query.severity) {
|
|
4830
|
-
const sev =
|
|
4930
|
+
const sev = import_types22.PolicySeveritySchema.safeParse(req2.query.severity);
|
|
4831
4931
|
if (!sev.success) {
|
|
4832
4932
|
return reply.code(400).send({
|
|
4833
4933
|
error: "invalid severity",
|
|
@@ -4844,7 +4944,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4844
4944
|
scope.post("/policies/check", async (req2, reply) => {
|
|
4845
4945
|
const proj = resolveProject(registry, req2, reply);
|
|
4846
4946
|
if (!proj) return;
|
|
4847
|
-
const parsed =
|
|
4947
|
+
const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
|
|
4848
4948
|
if (!parsed.success) {
|
|
4849
4949
|
return reply.code(400).send({
|
|
4850
4950
|
error: "invalid /policies/check body",
|
|
@@ -4945,11 +5045,11 @@ async function buildApi(opts) {
|
|
|
4945
5045
|
// src/daemon.ts
|
|
4946
5046
|
init_otel();
|
|
4947
5047
|
function neatHomeFor(opts) {
|
|
4948
|
-
if (opts.neatHome && opts.neatHome.length > 0) return
|
|
5048
|
+
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path36.default.resolve(opts.neatHome);
|
|
4949
5049
|
const env = process.env.NEAT_HOME;
|
|
4950
|
-
if (env && env.length > 0) return
|
|
5050
|
+
if (env && env.length > 0) return import_node_path36.default.resolve(env);
|
|
4951
5051
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
4952
|
-
return
|
|
5052
|
+
return import_node_path36.default.join(home, ".neat");
|
|
4953
5053
|
}
|
|
4954
5054
|
function routeSpanToProject(serviceName, projects) {
|
|
4955
5055
|
if (!serviceName) return DEFAULT_PROJECT;
|
|
@@ -4962,7 +5062,7 @@ function routeSpanToProject(serviceName, projects) {
|
|
|
4962
5062
|
return DEFAULT_PROJECT;
|
|
4963
5063
|
}
|
|
4964
5064
|
async function bootstrapProject(entry2) {
|
|
4965
|
-
const paths = pathsForProject(entry2.name,
|
|
5065
|
+
const paths = pathsForProject(entry2.name, import_node_path36.default.join(entry2.path, "neat-out"));
|
|
4966
5066
|
try {
|
|
4967
5067
|
const stat = await import_node_fs21.promises.stat(entry2.path);
|
|
4968
5068
|
if (!stat.isDirectory()) {
|
|
@@ -5035,7 +5135,7 @@ async function startDaemon(opts = {}) {
|
|
|
5035
5135
|
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
5036
5136
|
);
|
|
5037
5137
|
}
|
|
5038
|
-
const pidPath =
|
|
5138
|
+
const pidPath = import_node_path36.default.join(home, "neatd.pid");
|
|
5039
5139
|
await writeAtomically(pidPath, `${process.pid}
|
|
5040
5140
|
`);
|
|
5041
5141
|
const slots = /* @__PURE__ */ new Map();
|
|
@@ -5199,7 +5299,7 @@ async function startDaemon(opts = {}) {
|
|
|
5199
5299
|
init_cjs_shims();
|
|
5200
5300
|
var import_node_child_process = require("child_process");
|
|
5201
5301
|
var import_node_net = __toESM(require("net"), 1);
|
|
5202
|
-
var
|
|
5302
|
+
var import_node_path37 = __toESM(require("path"), 1);
|
|
5203
5303
|
var DEFAULT_WEB_PORT = 6328;
|
|
5204
5304
|
async function assertPortFree(port) {
|
|
5205
5305
|
await new Promise((resolve, reject) => {
|
|
@@ -5227,10 +5327,10 @@ function resolveWebPackageDir() {
|
|
|
5227
5327
|
eval("require")
|
|
5228
5328
|
);
|
|
5229
5329
|
const pkgJsonPath = req.resolve("@neat.is/web/package.json");
|
|
5230
|
-
return
|
|
5330
|
+
return import_node_path37.default.dirname(pkgJsonPath);
|
|
5231
5331
|
}
|
|
5232
5332
|
function resolveStandaloneServerEntry(webDir) {
|
|
5233
|
-
return
|
|
5333
|
+
return import_node_path37.default.join(webDir, ".next/standalone/packages/web/server.js");
|
|
5234
5334
|
}
|
|
5235
5335
|
async function spawnWebUI(restPort) {
|
|
5236
5336
|
const portRaw = process.env.NEAT_WEB_PORT;
|
|
@@ -5255,7 +5355,7 @@ async function spawnWebUI(restPort) {
|
|
|
5255
5355
|
NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`
|
|
5256
5356
|
};
|
|
5257
5357
|
const child = (0, import_node_child_process.spawn)(process.execPath, [serverEntry], {
|
|
5258
|
-
cwd:
|
|
5358
|
+
cwd: import_node_path37.default.dirname(serverEntry),
|
|
5259
5359
|
env,
|
|
5260
5360
|
stdio: ["ignore", "inherit", "inherit"],
|
|
5261
5361
|
detached: false
|
|
@@ -5292,14 +5392,14 @@ async function spawnWebUI(restPort) {
|
|
|
5292
5392
|
// src/neatd.ts
|
|
5293
5393
|
function neatHome2() {
|
|
5294
5394
|
if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
|
|
5295
|
-
return
|
|
5395
|
+
return import_node_path38.default.resolve(process.env.NEAT_HOME);
|
|
5296
5396
|
}
|
|
5297
5397
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
5298
|
-
return
|
|
5398
|
+
return import_node_path38.default.join(home, ".neat");
|
|
5299
5399
|
}
|
|
5300
5400
|
async function readPid() {
|
|
5301
5401
|
try {
|
|
5302
|
-
const raw = await import_node_fs22.promises.readFile(
|
|
5402
|
+
const raw = await import_node_fs22.promises.readFile(import_node_path38.default.join(neatHome2(), "neatd.pid"), "utf8");
|
|
5303
5403
|
const n = Number.parseInt(raw.trim(), 10);
|
|
5304
5404
|
return Number.isFinite(n) ? n : null;
|
|
5305
5405
|
} catch {
|