@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/index.cjs
CHANGED
|
@@ -100,8 +100,8 @@ function reshapeGrpcRequest(req) {
|
|
|
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 }
|
|
@@ -362,12 +362,12 @@ function logSpanHandler(span) {
|
|
|
362
362
|
`otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
|
|
363
363
|
);
|
|
364
364
|
}
|
|
365
|
-
var
|
|
365
|
+
var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
366
366
|
var init_otel = __esm({
|
|
367
367
|
"src/otel.ts"() {
|
|
368
368
|
"use strict";
|
|
369
369
|
init_cjs_shims();
|
|
370
|
-
|
|
370
|
+
import_node_path35 = __toESM(require("path"), 1);
|
|
371
371
|
import_node_url2 = require("url");
|
|
372
372
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
373
373
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
@@ -794,11 +794,16 @@ init_cjs_shims();
|
|
|
794
794
|
var import_types = require("@neat.is/types");
|
|
795
795
|
var ROOT_CAUSE_MAX_DEPTH = 5;
|
|
796
796
|
var BLAST_RADIUS_DEFAULT_DEPTH = 10;
|
|
797
|
+
function isFrontierNode(graph, nodeId) {
|
|
798
|
+
if (!graph.hasNode(nodeId)) return false;
|
|
799
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
800
|
+
return attrs.type === import_types.NodeType.FrontierNode;
|
|
801
|
+
}
|
|
797
802
|
function bestEdgeBySource(graph, edgeIds) {
|
|
798
803
|
const best = /* @__PURE__ */ new Map();
|
|
799
804
|
for (const id of edgeIds) {
|
|
800
805
|
const e = graph.getEdgeAttributes(id);
|
|
801
|
-
if (e.
|
|
806
|
+
if (isFrontierNode(graph, e.source)) continue;
|
|
802
807
|
const cur = best.get(e.source);
|
|
803
808
|
if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
|
|
804
809
|
best.set(e.source, e);
|
|
@@ -810,7 +815,7 @@ function bestEdgeByTarget(graph, edgeIds) {
|
|
|
810
815
|
const best = /* @__PURE__ */ new Map();
|
|
811
816
|
for (const id of edgeIds) {
|
|
812
817
|
const e = graph.getEdgeAttributes(id);
|
|
813
|
-
if (e.
|
|
818
|
+
if (isFrontierNode(graph, e.target)) continue;
|
|
814
819
|
const cur = best.get(e.target);
|
|
815
820
|
if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
|
|
816
821
|
best.set(e.target, e);
|
|
@@ -822,8 +827,7 @@ var PROVENANCE_CEILING = {
|
|
|
822
827
|
OBSERVED: 1,
|
|
823
828
|
INFERRED: 0.7,
|
|
824
829
|
EXTRACTED: 0.5,
|
|
825
|
-
STALE: 0.3
|
|
826
|
-
FRONTIER: 0.3
|
|
830
|
+
STALE: 0.3
|
|
827
831
|
};
|
|
828
832
|
function volumeWeight(spanCount) {
|
|
829
833
|
if (!spanCount || spanCount <= 0) return 0.5;
|
|
@@ -876,19 +880,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
876
880
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
877
881
|
let best = { path: [start], edges: [] };
|
|
878
882
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
879
|
-
function step(node,
|
|
880
|
-
if (
|
|
881
|
-
best = { path: [...
|
|
883
|
+
function step(node, path37, edges) {
|
|
884
|
+
if (path37.length > best.path.length) {
|
|
885
|
+
best = { path: [...path37], edges: [...edges] };
|
|
882
886
|
}
|
|
883
|
-
if (
|
|
887
|
+
if (path37.length - 1 >= maxDepth) return;
|
|
884
888
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
885
889
|
for (const [srcId, edge] of incoming) {
|
|
886
890
|
if (visited.has(srcId)) continue;
|
|
887
891
|
visited.add(srcId);
|
|
888
|
-
|
|
892
|
+
path37.push(srcId);
|
|
889
893
|
edges.push(edge);
|
|
890
|
-
step(srcId,
|
|
891
|
-
|
|
894
|
+
step(srcId, path37, edges);
|
|
895
|
+
path37.pop();
|
|
892
896
|
edges.pop();
|
|
893
897
|
visited.delete(srcId);
|
|
894
898
|
}
|
|
@@ -1107,8 +1111,8 @@ var evaluateStructural = ({
|
|
|
1107
1111
|
for (const edgeId of graph.outboundEdges(id)) {
|
|
1108
1112
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1109
1113
|
if (e.type !== rule.edgeType) continue;
|
|
1110
|
-
if (e.provenance === import_types2.Provenance.FRONTIER) continue;
|
|
1111
1114
|
const target = graph.getNodeAttributes(e.target);
|
|
1115
|
+
if (target.type === import_types2.NodeType.FrontierNode) continue;
|
|
1112
1116
|
if (target.type === rule.toNodeType) {
|
|
1113
1117
|
satisfied = true;
|
|
1114
1118
|
break;
|
|
@@ -1227,8 +1231,8 @@ var evaluateCompatibility = ({
|
|
|
1227
1231
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
1228
1232
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1229
1233
|
if (e.type !== import_types2.EdgeType.CONNECTS_TO) continue;
|
|
1230
|
-
if (e.provenance === import_types2.Provenance.FRONTIER) continue;
|
|
1231
1234
|
const dbAttrs = graph.getNodeAttributes(e.target);
|
|
1235
|
+
if (dbAttrs.type === import_types2.NodeType.FrontierNode) continue;
|
|
1232
1236
|
if (dbAttrs.type !== import_types2.NodeType.DatabaseNode) continue;
|
|
1233
1237
|
const db = dbAttrs;
|
|
1234
1238
|
for (const pair of compatPairs()) {
|
|
@@ -1545,31 +1549,6 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
1545
1549
|
graph.addNode(id, node);
|
|
1546
1550
|
return id;
|
|
1547
1551
|
}
|
|
1548
|
-
function upsertFrontierEdge(graph, type, source, target, ts) {
|
|
1549
|
-
const id = (0, import_types3.frontierEdgeId)(source, target, type);
|
|
1550
|
-
if (graph.hasEdge(id)) {
|
|
1551
|
-
const existing = graph.getEdgeAttributes(id);
|
|
1552
|
-
const updated = {
|
|
1553
|
-
...existing,
|
|
1554
|
-
provenance: import_types3.Provenance.FRONTIER,
|
|
1555
|
-
lastObserved: ts,
|
|
1556
|
-
callCount: (existing.callCount ?? 0) + 1
|
|
1557
|
-
};
|
|
1558
|
-
graph.replaceEdgeAttributes(id, updated);
|
|
1559
|
-
return;
|
|
1560
|
-
}
|
|
1561
|
-
const edge = {
|
|
1562
|
-
id,
|
|
1563
|
-
source,
|
|
1564
|
-
target,
|
|
1565
|
-
type,
|
|
1566
|
-
provenance: import_types3.Provenance.FRONTIER,
|
|
1567
|
-
confidence: 1,
|
|
1568
|
-
lastObserved: ts,
|
|
1569
|
-
callCount: 1
|
|
1570
|
-
};
|
|
1571
|
-
graph.addEdgeWithKey(id, source, target, edge);
|
|
1572
|
-
}
|
|
1573
1552
|
function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
1574
1553
|
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
1575
1554
|
const id = makeObservedEdgeId(type, source, target);
|
|
@@ -1577,35 +1556,37 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
|
1577
1556
|
const existing = graph.getEdgeAttributes(id);
|
|
1578
1557
|
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
1579
1558
|
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
1559
|
+
const newSignal = {
|
|
1560
|
+
spanCount: newSpanCount,
|
|
1561
|
+
errorCount: newErrorCount,
|
|
1562
|
+
lastObservedAgeMs: 0
|
|
1563
|
+
};
|
|
1580
1564
|
const updated = {
|
|
1581
1565
|
...existing,
|
|
1582
1566
|
provenance: import_types3.Provenance.OBSERVED,
|
|
1583
1567
|
lastObserved: ts,
|
|
1584
1568
|
callCount: newSpanCount,
|
|
1585
|
-
signal:
|
|
1586
|
-
|
|
1587
|
-
errorCount: newErrorCount,
|
|
1588
|
-
lastObservedAgeMs: 0
|
|
1589
|
-
},
|
|
1590
|
-
confidence: 1
|
|
1569
|
+
signal: newSignal,
|
|
1570
|
+
confidence: (0, import_types3.confidenceForObservedSignal)(newSignal)
|
|
1591
1571
|
};
|
|
1592
1572
|
graph.replaceEdgeAttributes(id, updated);
|
|
1593
1573
|
return { edge: updated, created: false };
|
|
1594
1574
|
}
|
|
1575
|
+
const signal = {
|
|
1576
|
+
spanCount: 1,
|
|
1577
|
+
errorCount: isError ? 1 : 0,
|
|
1578
|
+
lastObservedAgeMs: 0
|
|
1579
|
+
};
|
|
1595
1580
|
const edge = {
|
|
1596
1581
|
id,
|
|
1597
1582
|
source,
|
|
1598
1583
|
target,
|
|
1599
1584
|
type,
|
|
1600
1585
|
provenance: import_types3.Provenance.OBSERVED,
|
|
1601
|
-
confidence:
|
|
1586
|
+
confidence: (0, import_types3.confidenceForObservedSignal)(signal),
|
|
1602
1587
|
lastObserved: ts,
|
|
1603
1588
|
callCount: 1,
|
|
1604
|
-
signal
|
|
1605
|
-
spanCount: 1,
|
|
1606
|
-
errorCount: isError ? 1 : 0,
|
|
1607
|
-
lastObservedAgeMs: 0
|
|
1608
|
-
}
|
|
1589
|
+
signal
|
|
1609
1590
|
};
|
|
1610
1591
|
graph.addEdgeWithKey(id, source, target, edge);
|
|
1611
1592
|
return { edge, created: true };
|
|
@@ -1653,18 +1634,28 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
1653
1634
|
await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(ctx.errorsPath), { recursive: true });
|
|
1654
1635
|
await import_node_fs3.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
1655
1636
|
}
|
|
1637
|
+
function sanitizeAttributes(attrs) {
|
|
1638
|
+
const out = {};
|
|
1639
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
1640
|
+
if (typeof v === "bigint") out[k] = v.toString();
|
|
1641
|
+
else out[k] = v;
|
|
1642
|
+
}
|
|
1643
|
+
return out;
|
|
1644
|
+
}
|
|
1656
1645
|
function buildErrorEventForReceiver(span) {
|
|
1657
1646
|
if (span.statusCode !== 2) return null;
|
|
1658
1647
|
const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
1648
|
+
const attrs = sanitizeAttributes(span.attributes);
|
|
1659
1649
|
return {
|
|
1660
1650
|
id: `${span.traceId}:${span.spanId}`,
|
|
1661
1651
|
timestamp: ts,
|
|
1662
1652
|
service: span.service,
|
|
1663
1653
|
traceId: span.traceId,
|
|
1664
1654
|
spanId: span.spanId,
|
|
1665
|
-
errorMessage: span.exception?.message ?? span.
|
|
1655
|
+
errorMessage: span.exception?.message ?? span.name ?? "unknown error",
|
|
1666
1656
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1667
1657
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1658
|
+
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
1668
1659
|
affectedNode: (0, import_types3.serviceId)(span.service)
|
|
1669
1660
|
};
|
|
1670
1661
|
}
|
|
@@ -1715,11 +1706,16 @@ async function handleSpan(ctx, span) {
|
|
|
1715
1706
|
affectedNode = targetId;
|
|
1716
1707
|
resolvedViaAddress = true;
|
|
1717
1708
|
} else if (!targetId) {
|
|
1718
|
-
const
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1709
|
+
const frontierNodeId = ensureFrontierNode(ctx.graph, host, ts);
|
|
1710
|
+
upsertObservedEdge(
|
|
1711
|
+
ctx.graph,
|
|
1712
|
+
import_types3.EdgeType.CALLS,
|
|
1713
|
+
sourceId,
|
|
1714
|
+
frontierNodeId,
|
|
1715
|
+
ts,
|
|
1716
|
+
isError
|
|
1717
|
+
);
|
|
1718
|
+
affectedNode = frontierNodeId;
|
|
1723
1719
|
resolvedViaAddress = true;
|
|
1724
1720
|
}
|
|
1725
1721
|
}
|
|
@@ -1741,15 +1737,17 @@ async function handleSpan(ctx, span) {
|
|
|
1741
1737
|
if (span.statusCode === 2) {
|
|
1742
1738
|
stitchTrace(ctx.graph, sourceId, ts);
|
|
1743
1739
|
if (ctx.writeErrorEventInline !== false) {
|
|
1740
|
+
const attrs = sanitizeAttributes(span.attributes);
|
|
1744
1741
|
const ev = {
|
|
1745
1742
|
id: `${span.traceId}:${span.spanId}`,
|
|
1746
1743
|
timestamp: ts,
|
|
1747
1744
|
service: span.service,
|
|
1748
1745
|
traceId: span.traceId,
|
|
1749
1746
|
spanId: span.spanId,
|
|
1750
|
-
errorMessage: span.exception?.message ?? span.
|
|
1747
|
+
errorMessage: span.exception?.message ?? span.name ?? "unknown error",
|
|
1751
1748
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1752
1749
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1750
|
+
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
1753
1751
|
affectedNode
|
|
1754
1752
|
};
|
|
1755
1753
|
await appendErrorEvent(ctx, ev);
|
|
@@ -1805,8 +1803,7 @@ function rewireFrontierEdges(graph, frontierId2, serviceId3) {
|
|
|
1805
1803
|
}
|
|
1806
1804
|
function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
1807
1805
|
graph.dropEdge(oldEdgeId);
|
|
1808
|
-
const
|
|
1809
|
-
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);
|
|
1806
|
+
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);
|
|
1810
1807
|
if (graph.hasEdge(newId)) {
|
|
1811
1808
|
const existing = graph.getEdgeAttributes(newId);
|
|
1812
1809
|
const merged = {
|
|
@@ -1821,8 +1818,7 @@ function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
|
1821
1818
|
...edge,
|
|
1822
1819
|
id: newId,
|
|
1823
1820
|
source: newSource,
|
|
1824
|
-
target: newTarget
|
|
1825
|
-
provenance: promotedProvenance
|
|
1821
|
+
target: newTarget
|
|
1826
1822
|
};
|
|
1827
1823
|
graph.addEdgeWithKey(newId, newSource, newTarget, rebuilt);
|
|
1828
1824
|
}
|
|
@@ -2232,6 +2228,23 @@ async function writeExtractionErrors(errors, errorsPath) {
|
|
|
2232
2228
|
const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2233
2229
|
await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
|
|
2234
2230
|
}
|
|
2231
|
+
var droppedSink = [];
|
|
2232
|
+
function noteExtractedDropped(edge) {
|
|
2233
|
+
droppedSink.push(edge);
|
|
2234
|
+
}
|
|
2235
|
+
function drainDroppedExtracted() {
|
|
2236
|
+
return droppedSink.splice(0, droppedSink.length);
|
|
2237
|
+
}
|
|
2238
|
+
function isRejectedLogEnabled() {
|
|
2239
|
+
const raw = process.env.NEAT_EXTRACTED_REJECTED_LOG;
|
|
2240
|
+
return raw === "1" || raw === "true";
|
|
2241
|
+
}
|
|
2242
|
+
async function writeRejectedExtracted(drops, rejectedPath) {
|
|
2243
|
+
if (drops.length === 0) return;
|
|
2244
|
+
await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(rejectedPath), { recursive: true });
|
|
2245
|
+
const lines = drops.map((d) => JSON.stringify({ ...d, ts: (/* @__PURE__ */ new Date()).toISOString() })).join("\n") + "\n";
|
|
2246
|
+
await import_node_fs7.promises.appendFile(rejectedPath, lines, "utf8");
|
|
2247
|
+
}
|
|
2235
2248
|
|
|
2236
2249
|
// src/extract/services.ts
|
|
2237
2250
|
var DEFAULT_SCAN_DEPTH = 5;
|
|
@@ -3170,6 +3183,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3170
3183
|
target: dbNode.id,
|
|
3171
3184
|
type: import_types7.EdgeType.CONNECTS_TO,
|
|
3172
3185
|
provenance: import_types7.Provenance.EXTRACTED,
|
|
3186
|
+
confidence: (0, import_types7.confidenceForExtracted)("structural"),
|
|
3173
3187
|
// ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
|
|
3174
3188
|
// Ghost-edge cleanup keys retirement on this; the conditional
|
|
3175
3189
|
// sourceFile spread that used to live here was a v0.1.x leftover.
|
|
@@ -3244,6 +3258,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3244
3258
|
target: node.id,
|
|
3245
3259
|
type: import_types8.EdgeType.CONFIGURED_BY,
|
|
3246
3260
|
provenance: import_types8.Provenance.EXTRACTED,
|
|
3261
|
+
confidence: (0, import_types8.confidenceForExtracted)("structural"),
|
|
3247
3262
|
evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
|
|
3248
3263
|
};
|
|
3249
3264
|
if (!graph.hasEdge(edge.id)) {
|
|
@@ -3398,17 +3413,32 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3398
3413
|
for (const [targetId, evidenceFile] of seenTargets) {
|
|
3399
3414
|
const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
|
|
3400
3415
|
const line = lineOf(fileContent, `//${evidenceFile.host}`);
|
|
3416
|
+
const confidence = (0, import_types9.confidenceForExtracted)("hostname-shape-match");
|
|
3417
|
+
const ev = {
|
|
3418
|
+
file: import_node_path20.default.relative(service.dir, evidenceFile.file),
|
|
3419
|
+
line,
|
|
3420
|
+
snippet: snippet(fileContent, line)
|
|
3421
|
+
};
|
|
3422
|
+
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS);
|
|
3423
|
+
if (!(0, import_types9.passesExtractedFloor)(confidence)) {
|
|
3424
|
+
noteExtractedDropped({
|
|
3425
|
+
source: service.node.id,
|
|
3426
|
+
target: targetId,
|
|
3427
|
+
type: import_types9.EdgeType.CALLS,
|
|
3428
|
+
confidence,
|
|
3429
|
+
confidenceKind: "hostname-shape-match",
|
|
3430
|
+
evidence: ev
|
|
3431
|
+
});
|
|
3432
|
+
continue;
|
|
3433
|
+
}
|
|
3401
3434
|
const edge = {
|
|
3402
|
-
id:
|
|
3435
|
+
id: edgeId,
|
|
3403
3436
|
source: service.node.id,
|
|
3404
3437
|
target: targetId,
|
|
3405
3438
|
type: import_types9.EdgeType.CALLS,
|
|
3406
3439
|
provenance: import_types9.Provenance.EXTRACTED,
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
line,
|
|
3410
|
-
snippet: snippet(fileContent, line)
|
|
3411
|
-
}
|
|
3440
|
+
confidence,
|
|
3441
|
+
evidence: ev
|
|
3412
3442
|
};
|
|
3413
3443
|
if (!graph.hasEdge(edge.id)) {
|
|
3414
3444
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3447,6 +3477,10 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3447
3477
|
name: topic,
|
|
3448
3478
|
kind: "kafka-topic",
|
|
3449
3479
|
edgeType,
|
|
3480
|
+
// `producer.send({topic: 'x'})` / `consumer.subscribe({topic: 'x'})` —
|
|
3481
|
+
// framework-aware (kafkajs / node-rdkafka shape). Verified-call-site
|
|
3482
|
+
// tier (ADR-066).
|
|
3483
|
+
confidenceKind: "verified-call-site",
|
|
3450
3484
|
evidence: {
|
|
3451
3485
|
file: import_node_path21.default.relative(serviceDir, file.path),
|
|
3452
3486
|
line,
|
|
@@ -3479,6 +3513,10 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3479
3513
|
name: host,
|
|
3480
3514
|
kind: "redis",
|
|
3481
3515
|
edgeType: "CALLS",
|
|
3516
|
+
// `redis://host` URL literal — the scheme is structural support, but no
|
|
3517
|
+
// call expression is verified to wire it through. URL-with-structural-
|
|
3518
|
+
// support tier (ADR-066).
|
|
3519
|
+
confidenceKind: "url-with-structural-support",
|
|
3482
3520
|
evidence: {
|
|
3483
3521
|
file: import_node_path22.default.relative(serviceDir, file.path),
|
|
3484
3522
|
line,
|
|
@@ -3520,6 +3558,10 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3520
3558
|
name,
|
|
3521
3559
|
kind,
|
|
3522
3560
|
edgeType: "CALLS",
|
|
3561
|
+
// SDK marker (S3Client, GetCommand, etc.) plus a Bucket/TableName
|
|
3562
|
+
// literal — framework-aware recognizer, verified-call-site tier
|
|
3563
|
+
// (ADR-066).
|
|
3564
|
+
confidenceKind: "verified-call-site",
|
|
3523
3565
|
evidence: {
|
|
3524
3566
|
file: import_node_path23.default.relative(serviceDir, file.path),
|
|
3525
3567
|
line,
|
|
@@ -3599,6 +3641,10 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3599
3641
|
name,
|
|
3600
3642
|
kind,
|
|
3601
3643
|
edgeType: "CALLS",
|
|
3644
|
+
// `new <Name>Client(...)` with @aws-sdk/* or @grpc/grpc-js import
|
|
3645
|
+
// context — import-aware classification per #238. Verified-call-site
|
|
3646
|
+
// tier (ADR-066).
|
|
3647
|
+
confidenceKind: "verified-call-site",
|
|
3602
3648
|
evidence: {
|
|
3603
3649
|
file: import_node_path24.default.relative(serviceDir, file.path),
|
|
3604
3650
|
line,
|
|
@@ -3659,6 +3705,18 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3659
3705
|
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
|
|
3660
3706
|
if (seenEdges.has(edgeId)) continue;
|
|
3661
3707
|
seenEdges.add(edgeId);
|
|
3708
|
+
const confidence = (0, import_types14.confidenceForExtracted)(ep.confidenceKind);
|
|
3709
|
+
if (!(0, import_types14.passesExtractedFloor)(confidence)) {
|
|
3710
|
+
noteExtractedDropped({
|
|
3711
|
+
source: service.node.id,
|
|
3712
|
+
target: ep.infraId,
|
|
3713
|
+
type: edgeType,
|
|
3714
|
+
confidence,
|
|
3715
|
+
confidenceKind: ep.confidenceKind,
|
|
3716
|
+
evidence: ep.evidence
|
|
3717
|
+
});
|
|
3718
|
+
continue;
|
|
3719
|
+
}
|
|
3662
3720
|
if (!graph.hasEdge(edgeId)) {
|
|
3663
3721
|
const edge = {
|
|
3664
3722
|
id: edgeId,
|
|
@@ -3666,6 +3724,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3666
3724
|
target: ep.infraId,
|
|
3667
3725
|
type: edgeType,
|
|
3668
3726
|
provenance: import_types14.Provenance.EXTRACTED,
|
|
3727
|
+
confidence,
|
|
3669
3728
|
evidence: ep.evidence
|
|
3670
3729
|
};
|
|
3671
3730
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3785,6 +3844,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3785
3844
|
target: targetId,
|
|
3786
3845
|
type: import_types16.EdgeType.DEPENDS_ON,
|
|
3787
3846
|
provenance: import_types16.Provenance.EXTRACTED,
|
|
3847
|
+
confidence: (0, import_types16.confidenceForExtracted)("structural"),
|
|
3788
3848
|
evidence: { file: evidenceFile }
|
|
3789
3849
|
};
|
|
3790
3850
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3845,6 +3905,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3845
3905
|
target: node.id,
|
|
3846
3906
|
type: import_types17.EdgeType.RUNS_ON,
|
|
3847
3907
|
provenance: import_types17.Provenance.EXTRACTED,
|
|
3908
|
+
confidence: (0, import_types17.confidenceForExtracted)("structural"),
|
|
3848
3909
|
evidence: {
|
|
3849
3910
|
file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
|
|
3850
3911
|
}
|
|
@@ -3961,6 +4022,9 @@ async function addInfra(graph, scanPath, services) {
|
|
|
3961
4022
|
};
|
|
3962
4023
|
}
|
|
3963
4024
|
|
|
4025
|
+
// src/extract/index.ts
|
|
4026
|
+
var import_node_path30 = __toESM(require("path"), 1);
|
|
4027
|
+
|
|
3964
4028
|
// src/extract/retire.ts
|
|
3965
4029
|
init_cjs_shims();
|
|
3966
4030
|
var import_node_fs17 = require("fs");
|
|
@@ -4013,13 +4077,26 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
4013
4077
|
);
|
|
4014
4078
|
}
|
|
4015
4079
|
}
|
|
4080
|
+
const droppedEntries = drainDroppedExtracted();
|
|
4081
|
+
if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
|
|
4082
|
+
const rejectedPath = import_node_path30.default.join(import_node_path30.default.dirname(opts.errorsPath), "rejected.ndjson");
|
|
4083
|
+
try {
|
|
4084
|
+
await writeRejectedExtracted(droppedEntries, rejectedPath);
|
|
4085
|
+
} catch (err) {
|
|
4086
|
+
console.warn(
|
|
4087
|
+
`[neat] failed to write rejected extracted edges to ${rejectedPath}: ${err.message}`
|
|
4088
|
+
);
|
|
4089
|
+
}
|
|
4090
|
+
}
|
|
4016
4091
|
const result = {
|
|
4017
4092
|
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
4018
4093
|
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
4019
4094
|
frontiersPromoted,
|
|
4020
4095
|
extractionErrors: errorEntries.length,
|
|
4021
4096
|
errorEntries,
|
|
4022
|
-
ghostsRetired
|
|
4097
|
+
ghostsRetired,
|
|
4098
|
+
extractedDropped: droppedEntries.length,
|
|
4099
|
+
droppedEntries
|
|
4023
4100
|
};
|
|
4024
4101
|
emitNeatEvent({
|
|
4025
4102
|
type: "extraction-complete",
|
|
@@ -4037,8 +4114,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
4037
4114
|
// src/persist.ts
|
|
4038
4115
|
init_cjs_shims();
|
|
4039
4116
|
var import_node_fs18 = require("fs");
|
|
4040
|
-
var
|
|
4041
|
-
var
|
|
4117
|
+
var import_node_path31 = __toESM(require("path"), 1);
|
|
4118
|
+
var import_types19 = require("@neat.is/types");
|
|
4119
|
+
var SCHEMA_VERSION = 3;
|
|
4042
4120
|
function migrateV1ToV2(payload) {
|
|
4043
4121
|
const nodes = payload.graph.nodes;
|
|
4044
4122
|
if (Array.isArray(nodes)) {
|
|
@@ -4050,8 +4128,27 @@ function migrateV1ToV2(payload) {
|
|
|
4050
4128
|
}
|
|
4051
4129
|
return { ...payload, schemaVersion: 2 };
|
|
4052
4130
|
}
|
|
4131
|
+
function migrateV2ToV3(payload) {
|
|
4132
|
+
const edges = payload.graph.edges;
|
|
4133
|
+
if (Array.isArray(edges)) {
|
|
4134
|
+
for (const edge of edges) {
|
|
4135
|
+
const attrs = edge.attributes;
|
|
4136
|
+
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
4137
|
+
attrs.provenance = import_types19.Provenance.OBSERVED;
|
|
4138
|
+
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
4139
|
+
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
4140
|
+
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
4141
|
+
if (type && source && target) {
|
|
4142
|
+
const newId = (0, import_types19.observedEdgeId)(source, target, type);
|
|
4143
|
+
attrs.id = newId;
|
|
4144
|
+
if (edge.key) edge.key = newId;
|
|
4145
|
+
}
|
|
4146
|
+
}
|
|
4147
|
+
}
|
|
4148
|
+
return { ...payload, schemaVersion: 3 };
|
|
4149
|
+
}
|
|
4053
4150
|
async function ensureDir(filePath) {
|
|
4054
|
-
await import_node_fs18.promises.mkdir(
|
|
4151
|
+
await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
|
|
4055
4152
|
}
|
|
4056
4153
|
async function saveGraphToDisk(graph, outPath) {
|
|
4057
4154
|
await ensureDir(outPath);
|
|
@@ -4076,6 +4173,9 @@ async function loadGraphFromDisk(graph, outPath) {
|
|
|
4076
4173
|
if (payload.schemaVersion === 1) {
|
|
4077
4174
|
payload = migrateV1ToV2(payload);
|
|
4078
4175
|
}
|
|
4176
|
+
if (payload.schemaVersion === 2) {
|
|
4177
|
+
payload = migrateV2ToV3(payload);
|
|
4178
|
+
}
|
|
4079
4179
|
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
4080
4180
|
throw new Error(
|
|
4081
4181
|
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
@@ -4122,11 +4222,11 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
4122
4222
|
init_cjs_shims();
|
|
4123
4223
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
4124
4224
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
4125
|
-
var
|
|
4225
|
+
var import_types22 = require("@neat.is/types");
|
|
4126
4226
|
|
|
4127
4227
|
// src/divergences.ts
|
|
4128
4228
|
init_cjs_shims();
|
|
4129
|
-
var
|
|
4229
|
+
var import_types20 = require("@neat.is/types");
|
|
4130
4230
|
function bucketKey(source, target, type) {
|
|
4131
4231
|
return `${type}|${source}|${target}`;
|
|
4132
4232
|
}
|
|
@@ -4134,25 +4234,22 @@ function bucketEdges(graph) {
|
|
|
4134
4234
|
const buckets = /* @__PURE__ */ new Map();
|
|
4135
4235
|
graph.forEachEdge((id, attrs) => {
|
|
4136
4236
|
const e = attrs;
|
|
4137
|
-
const parsed = (0,
|
|
4237
|
+
const parsed = (0, import_types20.parseEdgeId)(id);
|
|
4138
4238
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
4139
4239
|
const key = bucketKey(e.source, e.target, e.type);
|
|
4140
4240
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
4141
4241
|
switch (provenance) {
|
|
4142
|
-
case
|
|
4242
|
+
case import_types20.Provenance.EXTRACTED:
|
|
4143
4243
|
cur.extracted = e;
|
|
4144
4244
|
break;
|
|
4145
|
-
case
|
|
4245
|
+
case import_types20.Provenance.OBSERVED:
|
|
4146
4246
|
cur.observed = e;
|
|
4147
4247
|
break;
|
|
4148
|
-
case
|
|
4248
|
+
case import_types20.Provenance.INFERRED:
|
|
4149
4249
|
cur.inferred = e;
|
|
4150
4250
|
break;
|
|
4151
|
-
case import_types19.Provenance.FRONTIER:
|
|
4152
|
-
cur.frontier = e;
|
|
4153
|
-
break;
|
|
4154
4251
|
default:
|
|
4155
|
-
if (e.provenance ===
|
|
4252
|
+
if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
|
|
4156
4253
|
}
|
|
4157
4254
|
buckets.set(key, cur);
|
|
4158
4255
|
});
|
|
@@ -4161,15 +4258,7 @@ function bucketEdges(graph) {
|
|
|
4161
4258
|
function nodeIsFrontier(graph, nodeId) {
|
|
4162
4259
|
if (!graph.hasNode(nodeId)) return false;
|
|
4163
4260
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
4164
|
-
return attrs.type ===
|
|
4165
|
-
}
|
|
4166
|
-
function hasAnyObservedFromSource(graph, sourceId) {
|
|
4167
|
-
if (!graph.hasNode(sourceId)) return false;
|
|
4168
|
-
for (const edgeId of graph.outboundEdges(sourceId)) {
|
|
4169
|
-
const e = graph.getEdgeAttributes(edgeId);
|
|
4170
|
-
if (e.provenance === import_types19.Provenance.OBSERVED) return true;
|
|
4171
|
-
}
|
|
4172
|
-
return false;
|
|
4261
|
+
return attrs.type === import_types20.NodeType.FrontierNode;
|
|
4173
4262
|
}
|
|
4174
4263
|
function clampConfidence(n) {
|
|
4175
4264
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -4184,32 +4273,34 @@ function reasonForMissingExtracted(source, target, type) {
|
|
|
4184
4273
|
var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
|
|
4185
4274
|
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.";
|
|
4186
4275
|
var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
|
|
4276
|
+
function gradedConfidence(edge) {
|
|
4277
|
+
if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
|
|
4278
|
+
return clampConfidence(confidenceForEdge(edge));
|
|
4279
|
+
}
|
|
4187
4280
|
function detectMissingDivergences(graph, bucket) {
|
|
4188
4281
|
const out = [];
|
|
4189
4282
|
if (bucket.extracted && !bucket.observed) {
|
|
4190
4283
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
4191
|
-
const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
|
|
4192
4284
|
out.push({
|
|
4193
4285
|
type: "missing-observed",
|
|
4194
4286
|
source: bucket.source,
|
|
4195
4287
|
target: bucket.target,
|
|
4196
4288
|
edgeType: bucket.type,
|
|
4197
4289
|
extracted: bucket.extracted,
|
|
4198
|
-
confidence:
|
|
4290
|
+
confidence: gradedConfidence(bucket.extracted),
|
|
4199
4291
|
reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
|
|
4200
4292
|
recommendation: RECOMMENDATION_MISSING_OBSERVED
|
|
4201
4293
|
});
|
|
4202
4294
|
}
|
|
4203
4295
|
}
|
|
4204
4296
|
if (bucket.observed && !bucket.extracted) {
|
|
4205
|
-
const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
|
|
4206
4297
|
out.push({
|
|
4207
4298
|
type: "missing-extracted",
|
|
4208
4299
|
source: bucket.source,
|
|
4209
4300
|
target: bucket.target,
|
|
4210
4301
|
edgeType: bucket.type,
|
|
4211
4302
|
observed: bucket.observed,
|
|
4212
|
-
confidence:
|
|
4303
|
+
confidence: gradedConfidence(bucket.observed),
|
|
4213
4304
|
reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
|
|
4214
4305
|
recommendation: RECOMMENDATION_MISSING_EXTRACTED
|
|
4215
4306
|
});
|
|
@@ -4228,7 +4319,7 @@ function declaredHostFor(svc) {
|
|
|
4228
4319
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
4229
4320
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4230
4321
|
const e = graph.getEdgeAttributes(edgeId);
|
|
4231
|
-
if (e.type ===
|
|
4322
|
+
if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
|
|
4232
4323
|
return true;
|
|
4233
4324
|
}
|
|
4234
4325
|
}
|
|
@@ -4241,10 +4332,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
4241
4332
|
const out = [];
|
|
4242
4333
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4243
4334
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4244
|
-
if (edge.type !==
|
|
4245
|
-
if (edge.provenance !==
|
|
4335
|
+
if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
|
|
4336
|
+
if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
|
|
4246
4337
|
const target = graph.getNodeAttributes(edge.target);
|
|
4247
|
-
if (target.type !==
|
|
4338
|
+
if (target.type !== import_types20.NodeType.DatabaseNode) continue;
|
|
4248
4339
|
const observedHost = target.host?.trim();
|
|
4249
4340
|
if (!observedHost) continue;
|
|
4250
4341
|
if (observedHost === declaredHost) continue;
|
|
@@ -4266,10 +4357,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
4266
4357
|
const deps = svc.dependencies ?? {};
|
|
4267
4358
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4268
4359
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4269
|
-
if (edge.type !==
|
|
4270
|
-
if (edge.provenance !==
|
|
4360
|
+
if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
|
|
4361
|
+
if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
|
|
4271
4362
|
const target = graph.getNodeAttributes(edge.target);
|
|
4272
|
-
if (target.type !==
|
|
4363
|
+
if (target.type !== import_types20.NodeType.DatabaseNode) continue;
|
|
4273
4364
|
for (const pair of compatPairs()) {
|
|
4274
4365
|
if (pair.engine !== target.engine) continue;
|
|
4275
4366
|
const declared = deps[pair.driver];
|
|
@@ -4330,7 +4421,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4330
4421
|
}
|
|
4331
4422
|
graph.forEachNode((nodeId, attrs) => {
|
|
4332
4423
|
const n = attrs;
|
|
4333
|
-
if (n.type !==
|
|
4424
|
+
if (n.type !== import_types20.NodeType.ServiceNode) return;
|
|
4334
4425
|
const svc = n;
|
|
4335
4426
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4336
4427
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
@@ -4348,13 +4439,22 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4348
4439
|
const target = opts.node;
|
|
4349
4440
|
filtered = filtered.filter((d) => involvesNode(d, target));
|
|
4350
4441
|
}
|
|
4442
|
+
const TYPE_LEADERSHIP = {
|
|
4443
|
+
"missing-extracted": 0,
|
|
4444
|
+
"missing-observed": 1,
|
|
4445
|
+
"version-mismatch": 2,
|
|
4446
|
+
"host-mismatch": 3,
|
|
4447
|
+
"compat-violation": 4
|
|
4448
|
+
};
|
|
4351
4449
|
filtered.sort((a, b) => {
|
|
4352
4450
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
4451
|
+
const lead = TYPE_LEADERSHIP[a.type] - TYPE_LEADERSHIP[b.type];
|
|
4452
|
+
if (lead !== 0) return lead;
|
|
4353
4453
|
if (a.type !== b.type) return a.type.localeCompare(b.type);
|
|
4354
4454
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4355
4455
|
return a.target.localeCompare(b.target);
|
|
4356
4456
|
});
|
|
4357
|
-
return
|
|
4457
|
+
return import_types20.DivergenceResultSchema.parse({
|
|
4358
4458
|
divergences: filtered,
|
|
4359
4459
|
totalAffected: filtered.length,
|
|
4360
4460
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -4440,23 +4540,23 @@ function canonicalJson(value) {
|
|
|
4440
4540
|
|
|
4441
4541
|
// src/projects.ts
|
|
4442
4542
|
init_cjs_shims();
|
|
4443
|
-
var
|
|
4543
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
4444
4544
|
function pathsForProject(project, baseDir) {
|
|
4445
4545
|
if (project === DEFAULT_PROJECT) {
|
|
4446
4546
|
return {
|
|
4447
|
-
snapshotPath:
|
|
4448
|
-
errorsPath:
|
|
4449
|
-
staleEventsPath:
|
|
4450
|
-
embeddingsCachePath:
|
|
4451
|
-
policyViolationsPath:
|
|
4547
|
+
snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
|
|
4548
|
+
errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
|
|
4549
|
+
staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
|
|
4550
|
+
embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
|
|
4551
|
+
policyViolationsPath: import_node_path32.default.join(baseDir, "policy-violations.ndjson")
|
|
4452
4552
|
};
|
|
4453
4553
|
}
|
|
4454
4554
|
return {
|
|
4455
|
-
snapshotPath:
|
|
4456
|
-
errorsPath:
|
|
4457
|
-
staleEventsPath:
|
|
4458
|
-
embeddingsCachePath:
|
|
4459
|
-
policyViolationsPath:
|
|
4555
|
+
snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
|
|
4556
|
+
errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
|
|
4557
|
+
staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
4558
|
+
embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
|
|
4559
|
+
policyViolationsPath: import_node_path32.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
4460
4560
|
};
|
|
4461
4561
|
}
|
|
4462
4562
|
var Projects = class {
|
|
@@ -4494,23 +4594,23 @@ var Projects = class {
|
|
|
4494
4594
|
init_cjs_shims();
|
|
4495
4595
|
var import_node_fs20 = require("fs");
|
|
4496
4596
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4497
|
-
var
|
|
4498
|
-
var
|
|
4597
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
4598
|
+
var import_types21 = require("@neat.is/types");
|
|
4499
4599
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
4500
4600
|
var LOCK_RETRY_MS = 50;
|
|
4501
4601
|
function neatHome() {
|
|
4502
4602
|
const override = process.env.NEAT_HOME;
|
|
4503
|
-
if (override && override.length > 0) return
|
|
4504
|
-
return
|
|
4603
|
+
if (override && override.length > 0) return import_node_path33.default.resolve(override);
|
|
4604
|
+
return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
|
|
4505
4605
|
}
|
|
4506
4606
|
function registryPath() {
|
|
4507
|
-
return
|
|
4607
|
+
return import_node_path33.default.join(neatHome(), "projects.json");
|
|
4508
4608
|
}
|
|
4509
4609
|
function registryLockPath() {
|
|
4510
|
-
return
|
|
4610
|
+
return import_node_path33.default.join(neatHome(), "projects.json.lock");
|
|
4511
4611
|
}
|
|
4512
4612
|
async function normalizeProjectPath(input) {
|
|
4513
|
-
const resolved =
|
|
4613
|
+
const resolved = import_node_path33.default.resolve(input);
|
|
4514
4614
|
try {
|
|
4515
4615
|
return await import_node_fs20.promises.realpath(resolved);
|
|
4516
4616
|
} catch {
|
|
@@ -4518,7 +4618,7 @@ async function normalizeProjectPath(input) {
|
|
|
4518
4618
|
}
|
|
4519
4619
|
}
|
|
4520
4620
|
async function writeAtomically(target, contents) {
|
|
4521
|
-
await import_node_fs20.promises.mkdir(
|
|
4621
|
+
await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
|
|
4522
4622
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4523
4623
|
const fd = await import_node_fs20.promises.open(tmp, "w");
|
|
4524
4624
|
try {
|
|
@@ -4531,7 +4631,7 @@ async function writeAtomically(target, contents) {
|
|
|
4531
4631
|
}
|
|
4532
4632
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4533
4633
|
const deadline = Date.now() + timeoutMs;
|
|
4534
|
-
await import_node_fs20.promises.mkdir(
|
|
4634
|
+
await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
|
|
4535
4635
|
while (true) {
|
|
4536
4636
|
try {
|
|
4537
4637
|
const fd = await import_node_fs20.promises.open(lockPath, "wx");
|
|
@@ -4574,10 +4674,10 @@ async function readRegistry() {
|
|
|
4574
4674
|
throw err;
|
|
4575
4675
|
}
|
|
4576
4676
|
const parsed = JSON.parse(raw);
|
|
4577
|
-
return
|
|
4677
|
+
return import_types21.RegistryFileSchema.parse(parsed);
|
|
4578
4678
|
}
|
|
4579
4679
|
async function writeRegistry(reg) {
|
|
4580
|
-
const validated =
|
|
4680
|
+
const validated = import_types21.RegistryFileSchema.parse(reg);
|
|
4581
4681
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4582
4682
|
}
|
|
4583
4683
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -4837,11 +4937,11 @@ function registerRoutes(scope, ctx) {
|
|
|
4837
4937
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4838
4938
|
const parsed = [];
|
|
4839
4939
|
for (const c of candidates) {
|
|
4840
|
-
const r =
|
|
4940
|
+
const r = import_types22.DivergenceTypeSchema.safeParse(c);
|
|
4841
4941
|
if (!r.success) {
|
|
4842
4942
|
return reply.code(400).send({
|
|
4843
4943
|
error: `unknown divergence type "${c}"`,
|
|
4844
|
-
allowed:
|
|
4944
|
+
allowed: import_types22.DivergenceTypeSchema.options
|
|
4845
4945
|
});
|
|
4846
4946
|
}
|
|
4847
4947
|
parsed.push(r.data);
|
|
@@ -5025,7 +5125,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5025
5125
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
5026
5126
|
let violations = await log.readAll();
|
|
5027
5127
|
if (req.query.severity) {
|
|
5028
|
-
const sev =
|
|
5128
|
+
const sev = import_types22.PolicySeveritySchema.safeParse(req.query.severity);
|
|
5029
5129
|
if (!sev.success) {
|
|
5030
5130
|
return reply.code(400).send({
|
|
5031
5131
|
error: "invalid severity",
|
|
@@ -5042,7 +5142,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5042
5142
|
scope.post("/policies/check", async (req, reply) => {
|
|
5043
5143
|
const proj = resolveProject(registry, req, reply);
|
|
5044
5144
|
if (!proj) return;
|
|
5045
|
-
const parsed =
|
|
5145
|
+
const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
5046
5146
|
if (!parsed.success) {
|
|
5047
5147
|
return reply.code(400).send({
|
|
5048
5148
|
error: "invalid /policies/check body",
|
|
@@ -5147,14 +5247,14 @@ init_otel_grpc();
|
|
|
5147
5247
|
// src/daemon.ts
|
|
5148
5248
|
init_cjs_shims();
|
|
5149
5249
|
var import_node_fs21 = require("fs");
|
|
5150
|
-
var
|
|
5250
|
+
var import_node_path36 = __toESM(require("path"), 1);
|
|
5151
5251
|
init_otel();
|
|
5152
5252
|
function neatHomeFor(opts) {
|
|
5153
|
-
if (opts.neatHome && opts.neatHome.length > 0) return
|
|
5253
|
+
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path36.default.resolve(opts.neatHome);
|
|
5154
5254
|
const env = process.env.NEAT_HOME;
|
|
5155
|
-
if (env && env.length > 0) return
|
|
5255
|
+
if (env && env.length > 0) return import_node_path36.default.resolve(env);
|
|
5156
5256
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
5157
|
-
return
|
|
5257
|
+
return import_node_path36.default.join(home, ".neat");
|
|
5158
5258
|
}
|
|
5159
5259
|
function routeSpanToProject(serviceName, projects) {
|
|
5160
5260
|
if (!serviceName) return DEFAULT_PROJECT;
|
|
@@ -5167,7 +5267,7 @@ function routeSpanToProject(serviceName, projects) {
|
|
|
5167
5267
|
return DEFAULT_PROJECT;
|
|
5168
5268
|
}
|
|
5169
5269
|
async function bootstrapProject(entry) {
|
|
5170
|
-
const paths = pathsForProject(entry.name,
|
|
5270
|
+
const paths = pathsForProject(entry.name, import_node_path36.default.join(entry.path, "neat-out"));
|
|
5171
5271
|
try {
|
|
5172
5272
|
const stat = await import_node_fs21.promises.stat(entry.path);
|
|
5173
5273
|
if (!stat.isDirectory()) {
|
|
@@ -5240,7 +5340,7 @@ async function startDaemon(opts = {}) {
|
|
|
5240
5340
|
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
5241
5341
|
);
|
|
5242
5342
|
}
|
|
5243
|
-
const pidPath =
|
|
5343
|
+
const pidPath = import_node_path36.default.join(home, "neatd.pid");
|
|
5244
5344
|
await writeAtomically(pidPath, `${process.pid}
|
|
5245
5345
|
`);
|
|
5246
5346
|
const slots = /* @__PURE__ */ new Map();
|