@neat.is/core 0.7.9 → 0.8.0-rc.1
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-N5TPODCX.js → chunk-3PXRQH53.js} +905 -37
- package/dist/chunk-3PXRQH53.js.map +1 -0
- package/dist/{chunk-ILG3SMD5.js → chunk-OJ6BW63H.js} +6 -2
- package/dist/chunk-OJ6BW63H.js.map +1 -0
- package/dist/cli.cjs +1673 -270
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +689 -164
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +922 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +28 -2
- package/dist/index.d.ts +28 -2
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +922 -42
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +911 -39
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-ILG3SMD5.js.map +0 -1
- package/dist/chunk-N5TPODCX.js.map +0 -1
package/dist/neatd.cjs
CHANGED
|
@@ -1400,7 +1400,7 @@ var rootCauseShapes = {
|
|
|
1400
1400
|
[import_types.NodeType.FileNode]: fileRootCauseShape,
|
|
1401
1401
|
[import_types.NodeType.SymbolNode]: symbolRootCauseShape
|
|
1402
1402
|
};
|
|
1403
|
-
function
|
|
1403
|
+
function legacyRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
1404
1404
|
if (!graph.hasNode(errorNodeId)) return null;
|
|
1405
1405
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
1406
1406
|
const shape = rootCauseShapes[origin.type];
|
|
@@ -1657,7 +1657,9 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
1657
1657
|
dependencies: [],
|
|
1658
1658
|
observed: false,
|
|
1659
1659
|
inboundObservedCount: 0,
|
|
1660
|
-
hasExtractedOutbound: false
|
|
1660
|
+
hasExtractedOutbound: false,
|
|
1661
|
+
inboundVolume: 0,
|
|
1662
|
+
window: "lifetime"
|
|
1661
1663
|
});
|
|
1662
1664
|
}
|
|
1663
1665
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
@@ -1688,11 +1690,19 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
1688
1690
|
}
|
|
1689
1691
|
}
|
|
1690
1692
|
let inboundObservedCount = 0;
|
|
1693
|
+
let inboundVolume = 0;
|
|
1694
|
+
let inboundLastObserved;
|
|
1691
1695
|
for (const tgt of scope) {
|
|
1692
1696
|
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
1693
1697
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1694
1698
|
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1695
|
-
if (e.provenance === import_types.Provenance.OBSERVED)
|
|
1699
|
+
if (e.provenance === import_types.Provenance.OBSERVED) {
|
|
1700
|
+
inboundObservedCount += 1;
|
|
1701
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 1;
|
|
1702
|
+
if (e.lastObserved && (!inboundLastObserved || e.lastObserved > inboundLastObserved)) {
|
|
1703
|
+
inboundLastObserved = e.lastObserved;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1696
1706
|
}
|
|
1697
1707
|
}
|
|
1698
1708
|
dependencies.sort(
|
|
@@ -1703,7 +1713,291 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
1703
1713
|
dependencies,
|
|
1704
1714
|
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
1705
1715
|
inboundObservedCount,
|
|
1706
|
-
hasExtractedOutbound
|
|
1716
|
+
hasExtractedOutbound,
|
|
1717
|
+
// The signal is cumulative, so the honest window label is "lifetime" (ADR-190).
|
|
1718
|
+
inboundVolume,
|
|
1719
|
+
window: "lifetime",
|
|
1720
|
+
...inboundLastObserved ? { inboundLastObserved } : {}
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
var SATURATION_P95_MS = 1e3;
|
|
1724
|
+
function nodeScope(graph, nodeId) {
|
|
1725
|
+
const scope = [nodeId];
|
|
1726
|
+
if (!graph.hasNode(nodeId)) return scope;
|
|
1727
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
1728
|
+
if (attrs.type === import_types.NodeType.ServiceNode) {
|
|
1729
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
1730
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1731
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1732
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
1733
|
+
if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
return scope;
|
|
1737
|
+
}
|
|
1738
|
+
function incidentCountForNode(nodeId, incidents) {
|
|
1739
|
+
if (!incidents || incidents.length === 0) return 0;
|
|
1740
|
+
return incidents.filter((ev) => incidentMatchesNode(ev, nodeId)).length;
|
|
1741
|
+
}
|
|
1742
|
+
function nodeContext(graph, nodeId, incidents, now = Date.now()) {
|
|
1743
|
+
const scope = nodeScope(graph, nodeId);
|
|
1744
|
+
let errorsFromCallers = 0;
|
|
1745
|
+
let inboundVolume = 0;
|
|
1746
|
+
let outboundVolume = 0;
|
|
1747
|
+
let outboundErrors = 0;
|
|
1748
|
+
let latestInboundMs;
|
|
1749
|
+
let latencyP95Ms;
|
|
1750
|
+
let stale = false;
|
|
1751
|
+
for (const n of scope) {
|
|
1752
|
+
if (!graph.hasNode(n)) continue;
|
|
1753
|
+
for (const edgeId of graph.inboundEdges(n)) {
|
|
1754
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1755
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1756
|
+
errorsFromCallers += e.signal?.errorCount ?? 0;
|
|
1757
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
1758
|
+
if (e.provenance === import_types.Provenance.STALE) stale = true;
|
|
1759
|
+
const p95 = e.signal?.latencyMs?.p95;
|
|
1760
|
+
if (p95 !== void 0) latencyP95Ms = Math.max(latencyP95Ms ?? 0, p95);
|
|
1761
|
+
if (e.lastObserved) {
|
|
1762
|
+
const t = Date.parse(e.lastObserved);
|
|
1763
|
+
if (Number.isFinite(t)) latestInboundMs = Math.max(latestInboundMs ?? 0, t);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
1767
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1768
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1769
|
+
outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
1770
|
+
if (e.type === import_types.EdgeType.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
|
|
1771
|
+
if (e.provenance === import_types.Provenance.STALE) stale = true;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
const errorsEmittedHere = incidentCountForNode(nodeId, incidents) + outboundErrors;
|
|
1775
|
+
const lastObservedAgeMs = latestInboundMs !== void 0 ? Math.max(0, now - latestInboundMs) : void 0;
|
|
1776
|
+
return {
|
|
1777
|
+
errorsEmittedHere,
|
|
1778
|
+
errorsFromCallers,
|
|
1779
|
+
callCount: inboundVolume,
|
|
1780
|
+
outboundVolume,
|
|
1781
|
+
...lastObservedAgeMs !== void 0 ? { lastObservedAgeMs } : {},
|
|
1782
|
+
...latencyP95Ms !== void 0 ? { latencyP95Ms } : {},
|
|
1783
|
+
stale
|
|
1784
|
+
};
|
|
1785
|
+
}
|
|
1786
|
+
function isSaturated(ctx) {
|
|
1787
|
+
return ctx.latencyP95Ms !== void 0 && ctx.latencyP95Ms >= SATURATION_P95_MS;
|
|
1788
|
+
}
|
|
1789
|
+
function classifyNode(ctx) {
|
|
1790
|
+
if (ctx.errorsEmittedHere > 0) {
|
|
1791
|
+
if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
|
|
1792
|
+
return "symptom-only";
|
|
1793
|
+
}
|
|
1794
|
+
return "primary-failure";
|
|
1795
|
+
}
|
|
1796
|
+
if (ctx.errorsFromCallers > 0) return "symptom-only";
|
|
1797
|
+
return "unrelated";
|
|
1798
|
+
}
|
|
1799
|
+
function isVictimSeed(ctx) {
|
|
1800
|
+
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
1801
|
+
}
|
|
1802
|
+
function grainOf(graph, nodeId) {
|
|
1803
|
+
if (!graph.hasNode(nodeId)) return "unknown";
|
|
1804
|
+
const t = graph.getNodeAttributes(nodeId).type;
|
|
1805
|
+
if (t === import_types.NodeType.ServiceNode) return "service";
|
|
1806
|
+
if (t === import_types.NodeType.FileNode) return "file";
|
|
1807
|
+
if (t === import_types.NodeType.SymbolNode) return "symbol";
|
|
1808
|
+
return t;
|
|
1809
|
+
}
|
|
1810
|
+
function findPath(graph, from, to, direction, maxDepth) {
|
|
1811
|
+
if (!graph.hasNode(from) || !graph.hasNode(to)) return null;
|
|
1812
|
+
if (from === to) return { nodes: [from], edges: [] };
|
|
1813
|
+
const queue = [{ nodeId: from, depth: 0, nodes: [from], edges: [] }];
|
|
1814
|
+
const enqueued = /* @__PURE__ */ new Set([from]);
|
|
1815
|
+
while (queue.length > 0) {
|
|
1816
|
+
const frame = queue.shift();
|
|
1817
|
+
if (frame.depth >= maxDepth) continue;
|
|
1818
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
|
|
1819
|
+
const neighbours = [...best.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
1820
|
+
for (const [nid, edge] of neighbours) {
|
|
1821
|
+
if (nid === to) return { nodes: [...frame.nodes, nid], edges: [...frame.edges, edge] };
|
|
1822
|
+
if (enqueued.has(nid)) continue;
|
|
1823
|
+
enqueued.add(nid);
|
|
1824
|
+
queue.push({
|
|
1825
|
+
nodeId: nid,
|
|
1826
|
+
depth: frame.depth + 1,
|
|
1827
|
+
nodes: [...frame.nodes, nid],
|
|
1828
|
+
edges: [...frame.edges, edge]
|
|
1829
|
+
});
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
return null;
|
|
1833
|
+
}
|
|
1834
|
+
var EMPTY_CONTEXT = {
|
|
1835
|
+
errorsEmittedHere: 0,
|
|
1836
|
+
errorsFromCallers: 0,
|
|
1837
|
+
callCount: 0,
|
|
1838
|
+
outboundVolume: 0,
|
|
1839
|
+
stale: false
|
|
1840
|
+
};
|
|
1841
|
+
function expandNode(graph, nodeId, direction, incidents, now = Date.now()) {
|
|
1842
|
+
if (!graph.hasNode(nodeId)) {
|
|
1843
|
+
return import_types.ExpandResultSchema.parse({
|
|
1844
|
+
origin: nodeId,
|
|
1845
|
+
direction,
|
|
1846
|
+
node: { id: nodeId, classification: "unrelated", context: EMPTY_CONTEXT },
|
|
1847
|
+
neighbours: []
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
const ctx = nodeContext(graph, nodeId, incidents, now);
|
|
1851
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(nodeId));
|
|
1852
|
+
const neighbours = [];
|
|
1853
|
+
for (const [nid, edge] of best) {
|
|
1854
|
+
if (edge.type === import_types.EdgeType.CONTAINS) continue;
|
|
1855
|
+
const nctx = nodeContext(graph, nid, incidents, now);
|
|
1856
|
+
neighbours.push({
|
|
1857
|
+
node: nid,
|
|
1858
|
+
edgeType: edge.type,
|
|
1859
|
+
provenance: edge.provenance,
|
|
1860
|
+
classification: classifyNode(nctx),
|
|
1861
|
+
context: nctx
|
|
1862
|
+
});
|
|
1863
|
+
}
|
|
1864
|
+
neighbours.sort((a, b) => a.node.localeCompare(b.node));
|
|
1865
|
+
return import_types.ExpandResultSchema.parse({
|
|
1866
|
+
origin: nodeId,
|
|
1867
|
+
direction,
|
|
1868
|
+
node: { id: nodeId, classification: classifyNode(ctx), context: ctx },
|
|
1869
|
+
neighbours
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
function relate(graph, a, b, maxDepth = ROOT_CAUSE_MAX_DEPTH) {
|
|
1873
|
+
const buildPath = (fp) => ({
|
|
1874
|
+
nodes: fp.nodes,
|
|
1875
|
+
edgeTypes: fp.edges.map((e) => e.type),
|
|
1876
|
+
provenance: fp.edges.map((e) => e.provenance),
|
|
1877
|
+
grain: fp.nodes.map((n) => grainOf(graph, n)),
|
|
1878
|
+
// The failure runs end to end when every hop carries error / latency / alert
|
|
1879
|
+
// signal — that is what turns reachability into cause-confirmation.
|
|
1880
|
+
carriesSignal: fp.edges.length > 0 && fp.edges.every(
|
|
1881
|
+
(e) => (e.signal?.errorCount ?? 0) > 0 || e.signal?.latencyMs !== void 0 || e.signal?.anomalous !== void 0
|
|
1882
|
+
)
|
|
1883
|
+
});
|
|
1884
|
+
if (!graph.hasNode(a) || !graph.hasNode(b)) {
|
|
1885
|
+
return import_types.RelateResultSchema.parse({
|
|
1886
|
+
a,
|
|
1887
|
+
b,
|
|
1888
|
+
related: false,
|
|
1889
|
+
direction: null,
|
|
1890
|
+
paths: [],
|
|
1891
|
+
note: !graph.hasNode(a) ? `node not found: ${a}` : `node not found: ${b}`
|
|
1892
|
+
});
|
|
1893
|
+
}
|
|
1894
|
+
const down = findPath(graph, a, b, "down", maxDepth);
|
|
1895
|
+
const up = findPath(graph, a, b, "up", maxDepth);
|
|
1896
|
+
if (!down && !up) {
|
|
1897
|
+
return import_types.RelateResultSchema.parse({
|
|
1898
|
+
a,
|
|
1899
|
+
b,
|
|
1900
|
+
related: false,
|
|
1901
|
+
direction: null,
|
|
1902
|
+
paths: [],
|
|
1903
|
+
note: `no path within ${maxDepth} hops`
|
|
1904
|
+
});
|
|
1905
|
+
}
|
|
1906
|
+
const paths = [];
|
|
1907
|
+
const direction = down ? "a->b" : "b->a";
|
|
1908
|
+
if (down) paths.push(buildPath(down));
|
|
1909
|
+
if (up) paths.push(buildPath(up));
|
|
1910
|
+
const endpointsFine = grainOf(graph, a) !== "service" && grainOf(graph, b) !== "service";
|
|
1911
|
+
const grainGap = endpointsFine && paths[0].grain.some((g) => g === "service");
|
|
1912
|
+
return import_types.RelateResultSchema.parse({
|
|
1913
|
+
a,
|
|
1914
|
+
b,
|
|
1915
|
+
related: true,
|
|
1916
|
+
direction,
|
|
1917
|
+
paths,
|
|
1918
|
+
...grainGap ? { grainGap: true } : {}
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
function findLoadOrigin(graph, alertNodeId, incidents, now) {
|
|
1922
|
+
const upstream = getBlastRadius(graph, alertNodeId).affectedNodes.map((n) => n.nodeId);
|
|
1923
|
+
let best = null;
|
|
1924
|
+
for (const nid of upstream) {
|
|
1925
|
+
if (nid === alertNodeId) continue;
|
|
1926
|
+
const ctx = nodeContext(graph, nid, incidents, now);
|
|
1927
|
+
if (ctx.outboundVolume === 0) continue;
|
|
1928
|
+
const isSource = ctx.callCount === 0;
|
|
1929
|
+
const better = !best || (isSource !== best.isSource ? isSource : ctx.outboundVolume !== best.ctx.outboundVolume ? ctx.outboundVolume > best.ctx.outboundVolume : nid < best.node);
|
|
1930
|
+
if (better) best = { node: nid, ctx, isSource };
|
|
1931
|
+
}
|
|
1932
|
+
return best ? { node: best.node, ctx: best.ctx } : null;
|
|
1933
|
+
}
|
|
1934
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents, opts) {
|
|
1935
|
+
const legacy = legacyRootCause(graph, errorNodeId, errorEvent, incidents);
|
|
1936
|
+
if (!legacy) return null;
|
|
1937
|
+
const navigation = opts?.navigation ?? process.env.NEAT_RCA_NAVIGATION !== "0";
|
|
1938
|
+
if (!navigation) return legacy;
|
|
1939
|
+
return enrichWithNavigation(graph, errorNodeId, legacy, incidents, opts?.now ?? Date.now());
|
|
1940
|
+
}
|
|
1941
|
+
function enrichWithNavigation(graph, errorNodeId, legacy, incidents, now) {
|
|
1942
|
+
const seedNode = legacy.rootCauseNode;
|
|
1943
|
+
const seedCtx = graph.hasNode(seedNode) ? nodeContext(graph, seedNode, incidents, now) : null;
|
|
1944
|
+
const lastProv = legacy.edgeProvenances[legacy.edgeProvenances.length - 1];
|
|
1945
|
+
const candidates = [];
|
|
1946
|
+
if (seedCtx && isVictimSeed(seedCtx)) {
|
|
1947
|
+
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
1948
|
+
if (origin) {
|
|
1949
|
+
const path70 = findPath(graph, errorNodeId, origin.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
1950
|
+
const originConfidence = confidenceFromMix(path70?.edges ?? [], now);
|
|
1951
|
+
candidates.push({
|
|
1952
|
+
node: origin.node,
|
|
1953
|
+
classification: "primary-failure",
|
|
1954
|
+
reason: `Highest-volume upstream source (${origin.ctx.outboundVolume} observed outbound calls) driving a saturated/stale subgraph; the alerting path decays downstream into a starved victim rather than a fault at the callee.`,
|
|
1955
|
+
context: origin.ctx,
|
|
1956
|
+
confidence: Math.max(0.3, Math.min(0.8, originConfidence || 0.5)),
|
|
1957
|
+
provenance: import_types.Provenance.OBSERVED
|
|
1958
|
+
});
|
|
1959
|
+
}
|
|
1960
|
+
const staleNote = seedCtx.stale ? "; the node has gone STALE" : "";
|
|
1961
|
+
const satNote = isSaturated(seedCtx) ? `; inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
1962
|
+
candidates.push({
|
|
1963
|
+
node: seedNode,
|
|
1964
|
+
classification: "symptom-only",
|
|
1965
|
+
reason: `Errors arrive from callers (${seedCtx.errorsFromCallers}) but none originate here${staleNote}${satNote} \u2014 a downstream victim of load, not the fault.`,
|
|
1966
|
+
context: seedCtx,
|
|
1967
|
+
confidence: Math.min(legacy.confidence, 0.4),
|
|
1968
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
1969
|
+
});
|
|
1970
|
+
} else {
|
|
1971
|
+
candidates.push({
|
|
1972
|
+
node: seedNode,
|
|
1973
|
+
classification: "primary-failure",
|
|
1974
|
+
reason: legacy.rootCauseReason,
|
|
1975
|
+
context: seedCtx ?? EMPTY_CONTEXT,
|
|
1976
|
+
confidence: legacy.confidence,
|
|
1977
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
1978
|
+
});
|
|
1979
|
+
}
|
|
1980
|
+
const top = candidates[0];
|
|
1981
|
+
let traversalPath = legacy.traversalPath;
|
|
1982
|
+
let edgeProvenances = legacy.edgeProvenances;
|
|
1983
|
+
if (top.node !== seedNode) {
|
|
1984
|
+
const path70 = findPath(graph, errorNodeId, top.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
1985
|
+
if (path70) {
|
|
1986
|
+
traversalPath = path70.nodes;
|
|
1987
|
+
edgeProvenances = path70.edges.map((e) => e.provenance);
|
|
1988
|
+
} else {
|
|
1989
|
+
traversalPath = [errorNodeId, top.node];
|
|
1990
|
+
edgeProvenances = [top.provenance ?? import_types.Provenance.OBSERVED];
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1994
|
+
rootCauseNode: top.node,
|
|
1995
|
+
rootCauseReason: top.reason,
|
|
1996
|
+
traversalPath,
|
|
1997
|
+
edgeProvenances,
|
|
1998
|
+
confidence: top.confidence,
|
|
1999
|
+
...legacy.fixRecommendation ? { fixRecommendation: legacy.fixRecommendation } : {},
|
|
2000
|
+
candidates
|
|
1707
2001
|
});
|
|
1708
2002
|
}
|
|
1709
2003
|
|
|
@@ -2229,6 +2523,7 @@ var import_yaml = require("yaml");
|
|
|
2229
2523
|
var import_types3 = require("@neat.is/types");
|
|
2230
2524
|
var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go", ".rb", ".php"]);
|
|
2231
2525
|
var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
|
|
2526
|
+
var JSON_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["app.json", "app.config.json", "eas.json"]);
|
|
2232
2527
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
2233
2528
|
"node_modules",
|
|
2234
2529
|
".git",
|
|
@@ -2268,6 +2563,7 @@ async function isPythonVenvDir(dir) {
|
|
|
2268
2563
|
function isConfigFile(name) {
|
|
2269
2564
|
const ext = import_node_path3.default.extname(name);
|
|
2270
2565
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
2566
|
+
if (JSON_CONFIG_FILENAMES.has(name)) return { match: true, fileType: "json" };
|
|
2271
2567
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
2272
2568
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
2273
2569
|
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
@@ -4584,6 +4880,63 @@ function columnIsObserved(col) {
|
|
|
4584
4880
|
return col.provenances.includes(import_types7.Provenance.OBSERVED);
|
|
4585
4881
|
}
|
|
4586
4882
|
|
|
4883
|
+
// src/latency-digest.ts
|
|
4884
|
+
init_cjs_shims();
|
|
4885
|
+
var SUB = 16;
|
|
4886
|
+
var MIN_EXP = -10;
|
|
4887
|
+
var MAX_EXP = 22;
|
|
4888
|
+
var OCTAVES = MAX_EXP - MIN_EXP + 1;
|
|
4889
|
+
var OVERFLOW_INDEX = OCTAVES * SUB + 1;
|
|
4890
|
+
function latencyBucketIndex(ms) {
|
|
4891
|
+
if (!Number.isFinite(ms) || ms <= 0) return 0;
|
|
4892
|
+
const e = Math.floor(Math.log2(ms));
|
|
4893
|
+
if (e < MIN_EXP) return 0;
|
|
4894
|
+
if (e > MAX_EXP) return OVERFLOW_INDEX;
|
|
4895
|
+
const base = 2 ** e;
|
|
4896
|
+
const raw = Math.floor((ms / base - 1) * SUB);
|
|
4897
|
+
const s = raw < 0 ? 0 : raw >= SUB ? SUB - 1 : raw;
|
|
4898
|
+
return (e - MIN_EXP) * SUB + s + 1;
|
|
4899
|
+
}
|
|
4900
|
+
function bucketRepresentativeMs(index) {
|
|
4901
|
+
if (index <= 0) return 0;
|
|
4902
|
+
if (index >= OVERFLOW_INDEX) return 2 ** (MAX_EXP + 1);
|
|
4903
|
+
const zeroBased = index - 1;
|
|
4904
|
+
const e = MIN_EXP + Math.floor(zeroBased / SUB);
|
|
4905
|
+
const s = zeroBased % SUB;
|
|
4906
|
+
const base = 2 ** e;
|
|
4907
|
+
const lower = base * (1 + s / SUB);
|
|
4908
|
+
const upper = base * (1 + (s + 1) / SUB);
|
|
4909
|
+
return (lower + upper) / 2;
|
|
4910
|
+
}
|
|
4911
|
+
function recordLatency(hist, ms) {
|
|
4912
|
+
const key = String(latencyBucketIndex(ms));
|
|
4913
|
+
hist[key] = (hist[key] ?? 0) + 1;
|
|
4914
|
+
return hist;
|
|
4915
|
+
}
|
|
4916
|
+
function quantile(hist, q) {
|
|
4917
|
+
const entries = Object.entries(hist).map(([k, c]) => [Number(k), c]).filter(([idx, c]) => Number.isFinite(idx) && c > 0).sort((a, b) => a[0] - b[0]);
|
|
4918
|
+
let total = 0;
|
|
4919
|
+
for (const [, c] of entries) total += c;
|
|
4920
|
+
if (total === 0) return 0;
|
|
4921
|
+
const rank = Math.max(1, Math.ceil(q * total));
|
|
4922
|
+
let cumulative = 0;
|
|
4923
|
+
for (const [idx, c] of entries) {
|
|
4924
|
+
cumulative += c;
|
|
4925
|
+
if (cumulative >= rank) return bucketRepresentativeMs(idx);
|
|
4926
|
+
}
|
|
4927
|
+
return bucketRepresentativeMs(entries[entries.length - 1][0]);
|
|
4928
|
+
}
|
|
4929
|
+
function round(ms) {
|
|
4930
|
+
return Math.round(ms * 100) / 100;
|
|
4931
|
+
}
|
|
4932
|
+
function latencyPercentiles(hist) {
|
|
4933
|
+
if (!hist) return void 0;
|
|
4934
|
+
let total = 0;
|
|
4935
|
+
for (const c of Object.values(hist)) total += c;
|
|
4936
|
+
if (total === 0) return void 0;
|
|
4937
|
+
return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
|
|
4938
|
+
}
|
|
4939
|
+
|
|
4587
4940
|
// src/ingest.ts
|
|
4588
4941
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
4589
4942
|
var DAY_MS = 24 * HOUR_MS;
|
|
@@ -5279,7 +5632,7 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
5279
5632
|
graph.addNode(id, node);
|
|
5280
5633
|
return id;
|
|
5281
5634
|
}
|
|
5282
|
-
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
|
|
5635
|
+
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence, durationMs) {
|
|
5283
5636
|
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
5284
5637
|
const grain = source.startsWith("file:") || source.startsWith("symbol:") ? "file" : "service";
|
|
5285
5638
|
const id = makeObservedEdgeId(type, source, target);
|
|
@@ -5287,10 +5640,15 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
5287
5640
|
const existing = graph.getEdgeAttributes(id);
|
|
5288
5641
|
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
5289
5642
|
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
5643
|
+
const latencyHist2 = durationMs !== void 0 ? recordLatency({ ...existing.signal?.latencyHist ?? {} }, durationMs) : existing.signal?.latencyHist;
|
|
5644
|
+
const latencyMs2 = latencyPercentiles(latencyHist2) ?? existing.signal?.latencyMs;
|
|
5290
5645
|
const newSignal = {
|
|
5291
5646
|
spanCount: newSpanCount,
|
|
5292
5647
|
errorCount: newErrorCount,
|
|
5293
|
-
lastObservedAgeMs: 0
|
|
5648
|
+
lastObservedAgeMs: 0,
|
|
5649
|
+
...latencyHist2 ? { latencyHist: latencyHist2 } : {},
|
|
5650
|
+
...latencyMs2 ? { latencyMs: latencyMs2 } : {},
|
|
5651
|
+
...existing.signal?.anomalous !== void 0 ? { anomalous: existing.signal.anomalous } : {}
|
|
5294
5652
|
};
|
|
5295
5653
|
const updated = {
|
|
5296
5654
|
...existing,
|
|
@@ -5305,10 +5663,14 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
5305
5663
|
graph.replaceEdgeAttributes(id, updated);
|
|
5306
5664
|
return { edge: updated, created: false };
|
|
5307
5665
|
}
|
|
5666
|
+
const latencyHist = durationMs !== void 0 ? recordLatency({}, durationMs) : void 0;
|
|
5667
|
+
const latencyMs = latencyHist ? latencyPercentiles(latencyHist) : void 0;
|
|
5308
5668
|
const signal = {
|
|
5309
5669
|
spanCount: 1,
|
|
5310
5670
|
errorCount: isError ? 1 : 0,
|
|
5311
|
-
lastObservedAgeMs: 0
|
|
5671
|
+
lastObservedAgeMs: 0,
|
|
5672
|
+
...latencyHist ? { latencyHist } : {},
|
|
5673
|
+
...latencyMs ? { latencyMs } : {}
|
|
5312
5674
|
};
|
|
5313
5675
|
const edge = {
|
|
5314
5676
|
id,
|
|
@@ -5372,6 +5734,21 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
5372
5734
|
await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(ctx.errorsPath), { recursive: true });
|
|
5373
5735
|
await import_node_fs7.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
5374
5736
|
}
|
|
5737
|
+
async function appendConnectorIncident(errorsPath, input) {
|
|
5738
|
+
const ev = {
|
|
5739
|
+
id: input.id,
|
|
5740
|
+
timestamp: input.timestamp,
|
|
5741
|
+
service: input.service,
|
|
5742
|
+
traceId: input.id,
|
|
5743
|
+
spanId: input.id,
|
|
5744
|
+
errorType: input.errorType,
|
|
5745
|
+
errorMessage: input.errorMessage,
|
|
5746
|
+
...input.attributes && Object.keys(input.attributes).length > 0 ? { attributes: input.attributes } : {},
|
|
5747
|
+
affectedNode: input.affectedNode
|
|
5748
|
+
};
|
|
5749
|
+
await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(errorsPath), { recursive: true });
|
|
5750
|
+
await import_node_fs7.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
5751
|
+
}
|
|
5375
5752
|
function incidentAffectedNode(span, graph, scanPath) {
|
|
5376
5753
|
const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
|
|
5377
5754
|
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
@@ -5518,6 +5895,7 @@ async function handleSpan(ctx, span) {
|
|
|
5518
5895
|
}
|
|
5519
5896
|
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
5520
5897
|
const isError = span.statusCode === 2;
|
|
5898
|
+
const durationMs = span.durationNanos > 0n ? Number(span.durationNanos) / 1e6 : void 0;
|
|
5521
5899
|
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
5522
5900
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
5523
5901
|
cacheSpanService(span, nowMs, callSite);
|
|
@@ -5556,7 +5934,8 @@ async function handleSpan(ctx, span) {
|
|
|
5556
5934
|
targetId,
|
|
5557
5935
|
ts,
|
|
5558
5936
|
isError,
|
|
5559
|
-
callSiteEvidence
|
|
5937
|
+
callSiteEvidence,
|
|
5938
|
+
durationMs
|
|
5560
5939
|
);
|
|
5561
5940
|
if (result) affectedNode = targetId;
|
|
5562
5941
|
if (span.dbSystem === "mongodb" && span.dbCollection) {
|
|
@@ -5568,7 +5947,8 @@ async function handleSpan(ctx, span) {
|
|
|
5568
5947
|
collectionId,
|
|
5569
5948
|
ts,
|
|
5570
5949
|
isError,
|
|
5571
|
-
callSiteEvidence
|
|
5950
|
+
callSiteEvidence,
|
|
5951
|
+
durationMs
|
|
5572
5952
|
);
|
|
5573
5953
|
}
|
|
5574
5954
|
if (span.dbTable) {
|
|
@@ -5580,7 +5960,8 @@ async function handleSpan(ctx, span) {
|
|
|
5580
5960
|
tableId,
|
|
5581
5961
|
ts,
|
|
5582
5962
|
isError,
|
|
5583
|
-
callSiteEvidence
|
|
5963
|
+
callSiteEvidence,
|
|
5964
|
+
durationMs
|
|
5584
5965
|
);
|
|
5585
5966
|
mergeObservedColumns(ctx.graph, tableId, span.dbColumns);
|
|
5586
5967
|
}
|
|
@@ -5599,7 +5980,8 @@ async function handleSpan(ctx, span) {
|
|
|
5599
5980
|
targetId,
|
|
5600
5981
|
ts,
|
|
5601
5982
|
isError,
|
|
5602
|
-
callSiteEvidence
|
|
5983
|
+
callSiteEvidence,
|
|
5984
|
+
durationMs
|
|
5603
5985
|
);
|
|
5604
5986
|
if (result) affectedNode = targetId;
|
|
5605
5987
|
} else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
|
|
@@ -5616,7 +5998,8 @@ async function handleSpan(ctx, span) {
|
|
|
5616
5998
|
targetId,
|
|
5617
5999
|
ts,
|
|
5618
6000
|
isError,
|
|
5619
|
-
callSiteEvidence
|
|
6001
|
+
callSiteEvidence,
|
|
6002
|
+
durationMs
|
|
5620
6003
|
);
|
|
5621
6004
|
if (result) affectedNode = targetId;
|
|
5622
6005
|
} else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
|
|
@@ -5628,7 +6011,8 @@ async function handleSpan(ctx, span) {
|
|
|
5628
6011
|
targetId,
|
|
5629
6012
|
ts,
|
|
5630
6013
|
isError,
|
|
5631
|
-
callSiteEvidence
|
|
6014
|
+
callSiteEvidence,
|
|
6015
|
+
durationMs
|
|
5632
6016
|
);
|
|
5633
6017
|
if (result) affectedNode = targetId;
|
|
5634
6018
|
} else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
|
|
@@ -5644,7 +6028,8 @@ async function handleSpan(ctx, span) {
|
|
|
5644
6028
|
targetId,
|
|
5645
6029
|
ts,
|
|
5646
6030
|
isError,
|
|
5647
|
-
callSiteEvidence
|
|
6031
|
+
callSiteEvidence,
|
|
6032
|
+
durationMs
|
|
5648
6033
|
);
|
|
5649
6034
|
if (result) affectedNode = targetId;
|
|
5650
6035
|
} else {
|
|
@@ -5660,7 +6045,8 @@ async function handleSpan(ctx, span) {
|
|
|
5660
6045
|
targetId,
|
|
5661
6046
|
ts,
|
|
5662
6047
|
isError,
|
|
5663
|
-
callSiteEvidence
|
|
6048
|
+
callSiteEvidence,
|
|
6049
|
+
durationMs
|
|
5664
6050
|
);
|
|
5665
6051
|
affectedNode = targetId;
|
|
5666
6052
|
resolvedViaAddress = true;
|
|
@@ -5673,7 +6059,8 @@ async function handleSpan(ctx, span) {
|
|
|
5673
6059
|
frontierNodeId,
|
|
5674
6060
|
ts,
|
|
5675
6061
|
isError,
|
|
5676
|
-
callSiteEvidence
|
|
6062
|
+
callSiteEvidence,
|
|
6063
|
+
durationMs
|
|
5677
6064
|
);
|
|
5678
6065
|
affectedNode = frontierNodeId;
|
|
5679
6066
|
resolvedViaAddress = true;
|
|
@@ -5699,7 +6086,8 @@ async function handleSpan(ctx, span) {
|
|
|
5699
6086
|
sourceId,
|
|
5700
6087
|
ts,
|
|
5701
6088
|
isError,
|
|
5702
|
-
fallbackEvidence
|
|
6089
|
+
fallbackEvidence,
|
|
6090
|
+
durationMs
|
|
5703
6091
|
);
|
|
5704
6092
|
}
|
|
5705
6093
|
}
|
|
@@ -5713,7 +6101,7 @@ async function handleSpan(ctx, span) {
|
|
|
5713
6101
|
);
|
|
5714
6102
|
if (routeNodeId) {
|
|
5715
6103
|
const routeSvc = ctx.graph.getNodeAttributes(routeNodeId).service;
|
|
5716
|
-
upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError);
|
|
6104
|
+
upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError, void 0, durationMs);
|
|
5717
6105
|
}
|
|
5718
6106
|
}
|
|
5719
6107
|
if (span.statusCode === 2) {
|
|
@@ -11182,8 +11570,41 @@ var import_tree_sitter14 = __toESM(require("tree-sitter"), 1);
|
|
|
11182
11570
|
var import_tree_sitter_go3 = __toESM(require("tree-sitter-go"), 1);
|
|
11183
11571
|
var import_types35 = require("@neat.is/types");
|
|
11184
11572
|
init_otel();
|
|
11185
|
-
var SQL_METHODS = /* @__PURE__ */ new Set(["Exec", "ExecContext", "Query", "QueryContext", "QueryRow", "QueryRowContext"]);
|
|
11186
11573
|
var PARSE_CHUNK10 = 16384;
|
|
11574
|
+
var DATABASE_SQL_METHODS = /* @__PURE__ */ new Set([
|
|
11575
|
+
"Query",
|
|
11576
|
+
"QueryContext",
|
|
11577
|
+
"QueryRow",
|
|
11578
|
+
"QueryRowContext",
|
|
11579
|
+
"Exec",
|
|
11580
|
+
"ExecContext",
|
|
11581
|
+
"Prepare",
|
|
11582
|
+
"PrepareContext"
|
|
11583
|
+
]);
|
|
11584
|
+
var SQLX_METHODS = /* @__PURE__ */ new Set([
|
|
11585
|
+
"Get",
|
|
11586
|
+
"Select",
|
|
11587
|
+
"Queryx",
|
|
11588
|
+
"QueryRowx",
|
|
11589
|
+
"NamedExec",
|
|
11590
|
+
"NamedQuery",
|
|
11591
|
+
"MustExec",
|
|
11592
|
+
"Preparex",
|
|
11593
|
+
"GetContext",
|
|
11594
|
+
"SelectContext"
|
|
11595
|
+
]);
|
|
11596
|
+
var DATABASE_SQL_IMPORT = "database/sql";
|
|
11597
|
+
var SQLX_IMPORT = "github.com/jmoiron/sqlx";
|
|
11598
|
+
function makeGoParser3() {
|
|
11599
|
+
const p = new import_tree_sitter14.default();
|
|
11600
|
+
p.setLanguage(import_tree_sitter_go3.default);
|
|
11601
|
+
return p;
|
|
11602
|
+
}
|
|
11603
|
+
function parseSource10(parser, source) {
|
|
11604
|
+
return parser.parse(
|
|
11605
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK10)
|
|
11606
|
+
);
|
|
11607
|
+
}
|
|
11187
11608
|
function walk7(node, visit) {
|
|
11188
11609
|
visit(node);
|
|
11189
11610
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -11191,25 +11612,54 @@ function walk7(node, visit) {
|
|
|
11191
11612
|
if (child) walk7(child, visit);
|
|
11192
11613
|
}
|
|
11193
11614
|
}
|
|
11615
|
+
function goStringLiteralValue(node) {
|
|
11616
|
+
if (!node) return null;
|
|
11617
|
+
if (node.type === "interpreted_string_literal" || node.type === "raw_string_literal") {
|
|
11618
|
+
const t = node.text;
|
|
11619
|
+
return t.length >= 2 ? t.slice(1, -1) : "";
|
|
11620
|
+
}
|
|
11621
|
+
return null;
|
|
11622
|
+
}
|
|
11623
|
+
function goImportsAny(root, names) {
|
|
11624
|
+
let found = false;
|
|
11625
|
+
walk7(root, (node) => {
|
|
11626
|
+
if (found || node.type !== "import_spec") return;
|
|
11627
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
11628
|
+
const value = goStringLiteralValue(node.namedChild(i));
|
|
11629
|
+
if (value !== null && names.has(value)) found = true;
|
|
11630
|
+
}
|
|
11631
|
+
});
|
|
11632
|
+
return found;
|
|
11633
|
+
}
|
|
11634
|
+
function firstStringLiteralArg(argsNode) {
|
|
11635
|
+
if (!argsNode) return null;
|
|
11636
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
11637
|
+
const value = goStringLiteralValue(argsNode.namedChild(i));
|
|
11638
|
+
if (value !== null) return value;
|
|
11639
|
+
}
|
|
11640
|
+
return null;
|
|
11641
|
+
}
|
|
11194
11642
|
function goSqlEndpointsFromFile(file, serviceDir) {
|
|
11195
11643
|
if (import_node_path47.default.extname(file.path) !== ".go") return [];
|
|
11196
|
-
|
|
11197
|
-
|
|
11198
|
-
const
|
|
11199
|
-
|
|
11200
|
-
);
|
|
11644
|
+
if (!file.content.includes(DATABASE_SQL_IMPORT) && !file.content.includes(SQLX_IMPORT)) return [];
|
|
11645
|
+
const tree = parseSource10(makeGoParser3(), file.content);
|
|
11646
|
+
const importsDatabaseSql = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([DATABASE_SQL_IMPORT]));
|
|
11647
|
+
const importsSqlx = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([SQLX_IMPORT]));
|
|
11648
|
+
if (!importsDatabaseSql && !importsSqlx) return [];
|
|
11201
11649
|
const out = [];
|
|
11202
11650
|
walk7(tree.rootNode, (node) => {
|
|
11203
11651
|
if (node.type !== "call_expression") return;
|
|
11204
11652
|
const fn = node.childForFieldName("function");
|
|
11205
11653
|
if (fn?.type !== "selector_expression") return;
|
|
11206
11654
|
const method = fn.childForFieldName("field")?.text;
|
|
11207
|
-
if (!method
|
|
11208
|
-
const
|
|
11209
|
-
if (!
|
|
11210
|
-
const sql =
|
|
11655
|
+
if (!method) return;
|
|
11656
|
+
const recognized = DATABASE_SQL_METHODS.has(method) || importsSqlx && SQLX_METHODS.has(method);
|
|
11657
|
+
if (!recognized) return;
|
|
11658
|
+
const sql = firstStringLiteralArg(node.childForFieldName("arguments"));
|
|
11659
|
+
if (sql === null) return;
|
|
11211
11660
|
const table = tableFromSqlStatement(sql);
|
|
11212
11661
|
if (!table) return;
|
|
11662
|
+
const columns = columnsFromSqlStatement(sql);
|
|
11213
11663
|
const line = node.startPosition.row + 1;
|
|
11214
11664
|
out.push({
|
|
11215
11665
|
infraId: (0, import_types35.infraId)("sql-table", table),
|
|
@@ -11217,7 +11667,12 @@ function goSqlEndpointsFromFile(file, serviceDir) {
|
|
|
11217
11667
|
kind: "sql-table",
|
|
11218
11668
|
edgeType: "CALLS",
|
|
11219
11669
|
confidenceKind: "verified-call-site",
|
|
11220
|
-
|
|
11670
|
+
...columns.length > 0 ? { columns } : {},
|
|
11671
|
+
evidence: {
|
|
11672
|
+
file: toPosix(import_node_path47.default.relative(serviceDir, file.path)),
|
|
11673
|
+
line,
|
|
11674
|
+
snippet: snippet(file.content, line)
|
|
11675
|
+
}
|
|
11221
11676
|
});
|
|
11222
11677
|
});
|
|
11223
11678
|
return out;
|
|
@@ -11231,12 +11686,12 @@ var import_tree_sitter_go4 = __toESM(require("tree-sitter-go"), 1);
|
|
|
11231
11686
|
var import_types36 = require("@neat.is/types");
|
|
11232
11687
|
var GORM_IMPORT_RE = /gorm\.io\/gorm/;
|
|
11233
11688
|
var PARSE_CHUNK11 = 16384;
|
|
11234
|
-
function
|
|
11689
|
+
function makeGoParser4() {
|
|
11235
11690
|
const p = new import_tree_sitter15.default();
|
|
11236
11691
|
p.setLanguage(import_tree_sitter_go4.default);
|
|
11237
11692
|
return p;
|
|
11238
11693
|
}
|
|
11239
|
-
function
|
|
11694
|
+
function parseSource11(parser, source) {
|
|
11240
11695
|
return parser.parse(
|
|
11241
11696
|
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK11)
|
|
11242
11697
|
);
|
|
@@ -11660,7 +12115,7 @@ function collectColumns(struct, structs, seen, prefix, out, emitted) {
|
|
|
11660
12115
|
function gormEndpointsFromFile(file, serviceDir) {
|
|
11661
12116
|
if (import_node_path48.default.extname(file.path) !== ".go") return [];
|
|
11662
12117
|
if (!GORM_IMPORT_RE.test(file.content)) return [];
|
|
11663
|
-
const tree =
|
|
12118
|
+
const tree = parseSource11(makeGoParser4(), file.content);
|
|
11664
12119
|
const { structs, models, tableFor } = analyze(tree);
|
|
11665
12120
|
const out = [];
|
|
11666
12121
|
const seenTables = /* @__PURE__ */ new Set();
|
|
@@ -11691,7 +12146,7 @@ function gormEndpointsFromFile(file, serviceDir) {
|
|
|
11691
12146
|
function gormForeignKeys(file, serviceDir) {
|
|
11692
12147
|
if (import_node_path48.default.extname(file.path) !== ".go") return [];
|
|
11693
12148
|
if (!GORM_IMPORT_RE.test(file.content)) return [];
|
|
11694
|
-
const tree =
|
|
12149
|
+
const tree = parseSource11(makeGoParser4(), file.content);
|
|
11695
12150
|
const { structs, models, tableFor } = analyze(tree);
|
|
11696
12151
|
const out = [];
|
|
11697
12152
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -13486,7 +13941,7 @@ var Projects = class {
|
|
|
13486
13941
|
init_cjs_shims();
|
|
13487
13942
|
var import_fastify2 = __toESM(require("fastify"), 1);
|
|
13488
13943
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
13489
|
-
var
|
|
13944
|
+
var import_types85 = require("@neat.is/types");
|
|
13490
13945
|
|
|
13491
13946
|
// src/extend/index.ts
|
|
13492
13947
|
init_cjs_shims();
|
|
@@ -14882,6 +15337,23 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
|
|
|
14882
15337
|
unresolved++;
|
|
14883
15338
|
continue;
|
|
14884
15339
|
}
|
|
15340
|
+
if (signal.incident) {
|
|
15341
|
+
ensureServiceNode(graph, resolved.serviceName, NO_ENV);
|
|
15342
|
+
if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
|
|
15343
|
+
unresolved++;
|
|
15344
|
+
continue;
|
|
15345
|
+
}
|
|
15346
|
+
await appendConnectorIncident(ctx.errorsPath, {
|
|
15347
|
+
id: signal.incident.id,
|
|
15348
|
+
timestamp: signal.incident.timestamp,
|
|
15349
|
+
service: signal.incident.service,
|
|
15350
|
+
errorType: signal.incident.errorType,
|
|
15351
|
+
errorMessage: signal.incident.errorMessage,
|
|
15352
|
+
...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
|
|
15353
|
+
affectedNode: resolved.targetNodeId
|
|
15354
|
+
});
|
|
15355
|
+
continue;
|
|
15356
|
+
}
|
|
14885
15357
|
if (resolved.ensureInfraNode) {
|
|
14886
15358
|
const { kind, name, provider } = resolved.ensureInfraNode;
|
|
14887
15359
|
ensureInfraNode(graph, kind, name, provider);
|
|
@@ -17116,6 +17588,338 @@ function createPlanetscaleConnector(graph, config, deps = {}) {
|
|
|
17116
17588
|
};
|
|
17117
17589
|
}
|
|
17118
17590
|
|
|
17591
|
+
// src/connectors/eas/index.ts
|
|
17592
|
+
init_cjs_shims();
|
|
17593
|
+
|
|
17594
|
+
// src/connectors/eas/client.ts
|
|
17595
|
+
init_cjs_shims();
|
|
17596
|
+
|
|
17597
|
+
// src/connectors/eas/types.ts
|
|
17598
|
+
init_cjs_shims();
|
|
17599
|
+
function readEasCredentials(raw) {
|
|
17600
|
+
const token = raw["token"];
|
|
17601
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
17602
|
+
throw new Error("eas connector: credentials.token (EXPO_TOKEN) must be a non-empty string");
|
|
17603
|
+
}
|
|
17604
|
+
return { token };
|
|
17605
|
+
}
|
|
17606
|
+
var EAS_STATUS_ERRORED = "ERRORED";
|
|
17607
|
+
var TRANSIENT_BUILD_PHASES = /* @__PURE__ */ new Set([
|
|
17608
|
+
"SPIN_UP_BUILDER",
|
|
17609
|
+
"PREPARE_CREDENTIALS",
|
|
17610
|
+
"RESTORE_CACHE",
|
|
17611
|
+
"UPLOAD_APPLICATION_ARCHIVE"
|
|
17612
|
+
]);
|
|
17613
|
+
var INTERNAL_ERROR_CODE = /INTERNAL_SERVER_ERROR|EAS_BUILD_.*INTERNAL|_INTERNAL_ERROR|UNKNOWN_ERROR/i;
|
|
17614
|
+
function isTransientFailure(err) {
|
|
17615
|
+
if (!err) return false;
|
|
17616
|
+
const phase = typeof err.buildPhase === "string" ? err.buildPhase : void 0;
|
|
17617
|
+
if (phase) {
|
|
17618
|
+
if (TRANSIENT_BUILD_PHASES.has(phase)) return true;
|
|
17619
|
+
if (/^SPIN_UP|_CREDENTIALS$|^RESTORE_CACHE/i.test(phase)) return true;
|
|
17620
|
+
}
|
|
17621
|
+
const code = typeof err.errorCode === "string" ? err.errorCode : void 0;
|
|
17622
|
+
if (code && INTERNAL_ERROR_CODE.test(code)) return true;
|
|
17623
|
+
return false;
|
|
17624
|
+
}
|
|
17625
|
+
var FIELD_SEP3 = "\0";
|
|
17626
|
+
var EAS_TARGET_KIND = "eas-build";
|
|
17627
|
+
function packEasTargetName(identity) {
|
|
17628
|
+
return [identity.serviceName, identity.phase].join(FIELD_SEP3);
|
|
17629
|
+
}
|
|
17630
|
+
function parseEasTargetName(targetName) {
|
|
17631
|
+
const sep = targetName.indexOf(FIELD_SEP3);
|
|
17632
|
+
if (sep === -1) return null;
|
|
17633
|
+
const serviceName = targetName.slice(0, sep);
|
|
17634
|
+
const phase = targetName.slice(sep + 1);
|
|
17635
|
+
if (!serviceName) return null;
|
|
17636
|
+
return { serviceName, phase };
|
|
17637
|
+
}
|
|
17638
|
+
|
|
17639
|
+
// src/connectors/eas/client.ts
|
|
17640
|
+
var DEFAULT_EAS_API_URL = "https://api.expo.dev/graphql";
|
|
17641
|
+
var DEFAULT_PAGE_SIZE = 50;
|
|
17642
|
+
var DEFAULT_MAX_PAGES = 10;
|
|
17643
|
+
var DEFAULT_MAX_LOG_BYTES = 16 * 1024;
|
|
17644
|
+
var DEFAULT_MAX_LOOKBACK_MS6 = 7 * 24 * 60 * 60 * 1e3;
|
|
17645
|
+
var BUILDS_QUERY = `
|
|
17646
|
+
query NeatEasErroredBuilds($appId: String!, $offset: Int!, $limit: Int!) {
|
|
17647
|
+
app {
|
|
17648
|
+
byId(appId: $appId) {
|
|
17649
|
+
builds(offset: $offset, limit: $limit, filter: { status: ERRORED }) {
|
|
17650
|
+
id
|
|
17651
|
+
status
|
|
17652
|
+
platform
|
|
17653
|
+
buildProfile
|
|
17654
|
+
gitCommitHash
|
|
17655
|
+
gitCommitMessage
|
|
17656
|
+
gitRef
|
|
17657
|
+
isGitWorkingTreeDirty
|
|
17658
|
+
createdAt
|
|
17659
|
+
completedAt
|
|
17660
|
+
error {
|
|
17661
|
+
buildPhase
|
|
17662
|
+
errorCode
|
|
17663
|
+
message
|
|
17664
|
+
docsUrl
|
|
17665
|
+
}
|
|
17666
|
+
logFileUrls
|
|
17667
|
+
}
|
|
17668
|
+
}
|
|
17669
|
+
}
|
|
17670
|
+
}
|
|
17671
|
+
`;
|
|
17672
|
+
async function easGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
|
|
17673
|
+
const res = await junctionFetch(
|
|
17674
|
+
apiUrl,
|
|
17675
|
+
{
|
|
17676
|
+
method: "POST",
|
|
17677
|
+
headers: {
|
|
17678
|
+
"Content-Type": "application/json",
|
|
17679
|
+
...bearerAuthHeader(token)
|
|
17680
|
+
},
|
|
17681
|
+
body: JSON.stringify({ query, variables })
|
|
17682
|
+
},
|
|
17683
|
+
// accountKey: the Expo app id — the per-(provider, accountKey) rate-limit
|
|
17684
|
+
// bucket (ADR-131), the closest thing this connector carries to "one account".
|
|
17685
|
+
{ provider: "eas", accountKey, ...fetchImpl ? { fetchImpl } : {} }
|
|
17686
|
+
);
|
|
17687
|
+
if (!res.ok) {
|
|
17688
|
+
throw new Error(`Expo GraphQL request failed: ${res.status} ${res.statusText}`);
|
|
17689
|
+
}
|
|
17690
|
+
const body = await res.json();
|
|
17691
|
+
if (body.errors && body.errors.length > 0) {
|
|
17692
|
+
throw new Error(`Expo GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
|
|
17693
|
+
}
|
|
17694
|
+
if (!body.data) throw new Error("Expo GraphQL response carried no data");
|
|
17695
|
+
return body.data;
|
|
17696
|
+
}
|
|
17697
|
+
async function fetchErroredBuilds(token, config, fetchImpl) {
|
|
17698
|
+
const apiUrl = config.apiUrl ?? DEFAULT_EAS_API_URL;
|
|
17699
|
+
const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
17700
|
+
const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
|
|
17701
|
+
const out = [];
|
|
17702
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17703
|
+
for (let page = 0; page < maxPages; page++) {
|
|
17704
|
+
const data = await easGraphQL(
|
|
17705
|
+
apiUrl,
|
|
17706
|
+
token,
|
|
17707
|
+
BUILDS_QUERY,
|
|
17708
|
+
{ appId: config.appId, offset: page * pageSize, limit: pageSize },
|
|
17709
|
+
config.appId,
|
|
17710
|
+
fetchImpl
|
|
17711
|
+
);
|
|
17712
|
+
const builds = data.app?.byId?.builds;
|
|
17713
|
+
if (!Array.isArray(builds)) break;
|
|
17714
|
+
let added = 0;
|
|
17715
|
+
for (const b of builds) {
|
|
17716
|
+
if (!b || typeof b.id !== "string" || b.id.length === 0) continue;
|
|
17717
|
+
if (b.status !== EAS_STATUS_ERRORED) continue;
|
|
17718
|
+
if (seen.has(b.id)) continue;
|
|
17719
|
+
seen.add(b.id);
|
|
17720
|
+
out.push(b);
|
|
17721
|
+
added++;
|
|
17722
|
+
}
|
|
17723
|
+
if (builds.length < pageSize) break;
|
|
17724
|
+
if (added === 0) break;
|
|
17725
|
+
}
|
|
17726
|
+
return out;
|
|
17727
|
+
}
|
|
17728
|
+
async function fetchBuildLogs(logFileUrls, maxBytes = DEFAULT_MAX_LOG_BYTES, fetchImpl) {
|
|
17729
|
+
if (!Array.isArray(logFileUrls) || logFileUrls.length === 0) return "";
|
|
17730
|
+
const doFetch = fetchImpl ?? fetch;
|
|
17731
|
+
const chunks = [];
|
|
17732
|
+
for (const url of logFileUrls) {
|
|
17733
|
+
if (typeof url !== "string" || url.length === 0) continue;
|
|
17734
|
+
try {
|
|
17735
|
+
const res = await doFetch(url);
|
|
17736
|
+
if (!res.ok) continue;
|
|
17737
|
+
chunks.push(await res.text());
|
|
17738
|
+
} catch {
|
|
17739
|
+
}
|
|
17740
|
+
}
|
|
17741
|
+
const joined = chunks.join("\n");
|
|
17742
|
+
return joined.length > maxBytes ? joined.slice(joined.length - maxBytes) : joined;
|
|
17743
|
+
}
|
|
17744
|
+
|
|
17745
|
+
// src/connectors/eas/map.ts
|
|
17746
|
+
init_cjs_shims();
|
|
17747
|
+
function buildEventTime(build) {
|
|
17748
|
+
if (typeof build.completedAt === "string" && build.completedAt.length > 0) return build.completedAt;
|
|
17749
|
+
if (typeof build.createdAt === "string" && build.createdAt.length > 0) return build.createdAt;
|
|
17750
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
17751
|
+
}
|
|
17752
|
+
function incidentMessage2(build) {
|
|
17753
|
+
const err = build.error ?? {};
|
|
17754
|
+
const phase = typeof err.buildPhase === "string" && err.buildPhase.length > 0 ? ` at ${err.buildPhase}` : "";
|
|
17755
|
+
const detail = typeof err.message === "string" && err.message.trim().length > 0 && err.message.trim() || typeof err.errorCode === "string" && err.errorCode.length > 0 && err.errorCode || "no error detail reported";
|
|
17756
|
+
let msg = `EAS build failed${phase}: ${detail}`;
|
|
17757
|
+
if (build.isGitWorkingTreeDirty === true) {
|
|
17758
|
+
msg += " (built from a dirty working tree \u2014 the commit may not represent what built)";
|
|
17759
|
+
}
|
|
17760
|
+
return msg;
|
|
17761
|
+
}
|
|
17762
|
+
function incidentAttributes(build) {
|
|
17763
|
+
const attrs = {};
|
|
17764
|
+
const err = build.error ?? {};
|
|
17765
|
+
const put = (k, v) => {
|
|
17766
|
+
if (typeof v === "string" && v.length === 0) return;
|
|
17767
|
+
if (v !== void 0 && v !== null) attrs[k] = v;
|
|
17768
|
+
};
|
|
17769
|
+
put("eas.buildId", build.id);
|
|
17770
|
+
put("eas.platform", build.platform ?? void 0);
|
|
17771
|
+
put("eas.buildProfile", build.buildProfile ?? void 0);
|
|
17772
|
+
put("eas.buildPhase", err.buildPhase ?? void 0);
|
|
17773
|
+
put("eas.errorCode", err.errorCode ?? void 0);
|
|
17774
|
+
put("eas.docsUrl", err.docsUrl ?? void 0);
|
|
17775
|
+
put("eas.gitCommitHash", build.gitCommitHash ?? void 0);
|
|
17776
|
+
put("eas.gitRef", build.gitRef ?? void 0);
|
|
17777
|
+
put("eas.gitCommitMessage", build.gitCommitMessage ?? void 0);
|
|
17778
|
+
if (typeof build.isGitWorkingTreeDirty === "boolean") {
|
|
17779
|
+
attrs["eas.gitWorkingTreeDirty"] = build.isGitWorkingTreeDirty;
|
|
17780
|
+
if (build.isGitWorkingTreeDirty) attrs["eas.confidence"] = "low";
|
|
17781
|
+
}
|
|
17782
|
+
put("eas.createdAt", build.createdAt ?? void 0);
|
|
17783
|
+
put("eas.completedAt", build.completedAt ?? void 0);
|
|
17784
|
+
if (typeof build.logsText === "string" && build.logsText.length > 0) {
|
|
17785
|
+
attrs["eas.logs"] = build.logsText;
|
|
17786
|
+
}
|
|
17787
|
+
return attrs;
|
|
17788
|
+
}
|
|
17789
|
+
function mapBuildToSignal(build, serviceName) {
|
|
17790
|
+
if (!build || typeof build !== "object") return null;
|
|
17791
|
+
if (build.status !== EAS_STATUS_ERRORED) return null;
|
|
17792
|
+
if (!build.error) return null;
|
|
17793
|
+
if (isTransientFailure(build.error)) return null;
|
|
17794
|
+
const timestamp = buildEventTime(build);
|
|
17795
|
+
const phase = typeof build.error.buildPhase === "string" ? build.error.buildPhase : "";
|
|
17796
|
+
return {
|
|
17797
|
+
targetKind: EAS_TARGET_KIND,
|
|
17798
|
+
targetName: packEasTargetName({ serviceName, phase }),
|
|
17799
|
+
// Incident-only — no edge, so no call/error count to replay.
|
|
17800
|
+
callCount: 0,
|
|
17801
|
+
errorCount: 0,
|
|
17802
|
+
lastObservedIso: timestamp,
|
|
17803
|
+
incident: {
|
|
17804
|
+
id: `eas:build:${build.id}`,
|
|
17805
|
+
timestamp,
|
|
17806
|
+
service: serviceName,
|
|
17807
|
+
errorType: "eas-build-failure",
|
|
17808
|
+
errorMessage: incidentMessage2(build),
|
|
17809
|
+
attributes: incidentAttributes(build)
|
|
17810
|
+
}
|
|
17811
|
+
};
|
|
17812
|
+
}
|
|
17813
|
+
function mapBuildsToSignals(builds, serviceName) {
|
|
17814
|
+
const out = [];
|
|
17815
|
+
for (const build of builds) {
|
|
17816
|
+
const signal = mapBuildToSignal(build, serviceName);
|
|
17817
|
+
if (signal) out.push(signal);
|
|
17818
|
+
}
|
|
17819
|
+
return out;
|
|
17820
|
+
}
|
|
17821
|
+
|
|
17822
|
+
// src/connectors/eas/resolve.ts
|
|
17823
|
+
init_cjs_shims();
|
|
17824
|
+
var import_types82 = require("@neat.is/types");
|
|
17825
|
+
var NO_ENV2 = "unknown";
|
|
17826
|
+
var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
|
|
17827
|
+
var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
|
|
17828
|
+
"READ_APP_CONFIG",
|
|
17829
|
+
"CONFIGURE_EXPO_UPDATES",
|
|
17830
|
+
"CALCULATE_EXPO_UPDATES_RUNTIME_VERSION"
|
|
17831
|
+
]);
|
|
17832
|
+
function configBasenamesForPhase(phase) {
|
|
17833
|
+
if (EAS_JSON_PHASES.has(phase)) return ["eas.json"];
|
|
17834
|
+
if (APP_CONFIG_PHASES.has(phase)) return ["app.json", "app.config.json"];
|
|
17835
|
+
return [];
|
|
17836
|
+
}
|
|
17837
|
+
function configNodeService(graph, configNodeId) {
|
|
17838
|
+
for (const edgeId of graph.inboundEdges(configNodeId)) {
|
|
17839
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
17840
|
+
if (edge.type !== import_types82.EdgeType.CONFIGURED_BY) continue;
|
|
17841
|
+
const parsed = (0, import_types82.parseFileId)(edge.source);
|
|
17842
|
+
if (parsed) return parsed.service;
|
|
17843
|
+
}
|
|
17844
|
+
return null;
|
|
17845
|
+
}
|
|
17846
|
+
function findConfigNode(graph, basenames, serviceName) {
|
|
17847
|
+
let scoped = null;
|
|
17848
|
+
let anyMatch = null;
|
|
17849
|
+
graph.forEachNode((id, attrs) => {
|
|
17850
|
+
if (scoped) return;
|
|
17851
|
+
const node = attrs;
|
|
17852
|
+
if (node.type !== import_types82.NodeType.ConfigNode) return;
|
|
17853
|
+
if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
|
|
17854
|
+
if (anyMatch === null) anyMatch = id;
|
|
17855
|
+
if (configNodeService(graph, id) === serviceName) scoped = id;
|
|
17856
|
+
});
|
|
17857
|
+
return scoped ?? anyMatch;
|
|
17858
|
+
}
|
|
17859
|
+
function createEasResolveTarget(graph) {
|
|
17860
|
+
return (signal) => {
|
|
17861
|
+
if (signal.targetKind !== EAS_TARGET_KIND) return null;
|
|
17862
|
+
const identity = parseEasTargetName(signal.targetName);
|
|
17863
|
+
if (!identity) return null;
|
|
17864
|
+
const { serviceName, phase } = identity;
|
|
17865
|
+
const basenames = configBasenamesForPhase(phase);
|
|
17866
|
+
if (basenames.length > 0) {
|
|
17867
|
+
const configNodeId = findConfigNode(graph, basenames, serviceName);
|
|
17868
|
+
if (configNodeId) {
|
|
17869
|
+
return { targetNodeId: configNodeId, serviceName, edgeType: import_types82.EdgeType.CALLS };
|
|
17870
|
+
}
|
|
17871
|
+
}
|
|
17872
|
+
return {
|
|
17873
|
+
targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
|
|
17874
|
+
serviceName,
|
|
17875
|
+
edgeType: import_types82.EdgeType.CALLS
|
|
17876
|
+
};
|
|
17877
|
+
};
|
|
17878
|
+
}
|
|
17879
|
+
|
|
17880
|
+
// src/connectors/eas/index.ts
|
|
17881
|
+
function isBuildSince(build, sinceIso) {
|
|
17882
|
+
const t = Date.parse(buildEventTime(build));
|
|
17883
|
+
const s = Date.parse(sinceIso);
|
|
17884
|
+
if (Number.isNaN(t) || Number.isNaN(s)) return true;
|
|
17885
|
+
return t > s;
|
|
17886
|
+
}
|
|
17887
|
+
function boundedSinceIso2(since, now, maxLookbackMs) {
|
|
17888
|
+
const floor = new Date(now.getTime() - maxLookbackMs);
|
|
17889
|
+
if (!since) return floor.toISOString();
|
|
17890
|
+
const sinceMs = new Date(since).getTime();
|
|
17891
|
+
if (Number.isNaN(sinceMs)) return floor.toISOString();
|
|
17892
|
+
return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
|
|
17893
|
+
}
|
|
17894
|
+
var EasConnector = class {
|
|
17895
|
+
constructor(config, fetchImpl) {
|
|
17896
|
+
this.config = config;
|
|
17897
|
+
this.fetchImpl = fetchImpl;
|
|
17898
|
+
}
|
|
17899
|
+
config;
|
|
17900
|
+
fetchImpl;
|
|
17901
|
+
provider = "eas";
|
|
17902
|
+
async poll(ctx) {
|
|
17903
|
+
const creds = readEasCredentials(ctx.credentials);
|
|
17904
|
+
const serviceName = this.config.serviceName ?? this.config.appId;
|
|
17905
|
+
const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS6;
|
|
17906
|
+
const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
|
|
17907
|
+
const builds = await fetchErroredBuilds(creds.token, this.config, this.fetchImpl);
|
|
17908
|
+
const fresh = builds.filter((b) => isBuildSince(b, sinceIso));
|
|
17909
|
+
const maxLogBytes = this.config.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
|
|
17910
|
+
for (const build of fresh) {
|
|
17911
|
+
build.logsText = await fetchBuildLogs(build.logFileUrls, maxLogBytes, this.fetchImpl);
|
|
17912
|
+
}
|
|
17913
|
+
return mapBuildsToSignals(fresh, serviceName);
|
|
17914
|
+
}
|
|
17915
|
+
};
|
|
17916
|
+
function createEasConnector(graph, config, fetchImpl) {
|
|
17917
|
+
return {
|
|
17918
|
+
connector: new EasConnector(config, fetchImpl),
|
|
17919
|
+
resolveTarget: createEasResolveTarget(graph)
|
|
17920
|
+
};
|
|
17921
|
+
}
|
|
17922
|
+
|
|
17119
17923
|
// src/connectors/registry.ts
|
|
17120
17924
|
var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
17121
17925
|
async function authProbe(input) {
|
|
@@ -17403,6 +18207,41 @@ var PROVIDER_DISPATCH = {
|
|
|
17403
18207
|
...fetchImpl ? { fetchImpl } : {}
|
|
17404
18208
|
});
|
|
17405
18209
|
}
|
|
18210
|
+
},
|
|
18211
|
+
eas: {
|
|
18212
|
+
provider: "eas",
|
|
18213
|
+
// The secret is a single robot-user EXPO_TOKEN; a single-string credential
|
|
18214
|
+
// maps to `token`. `appId` is non-secret config (connector-config.md §7.1).
|
|
18215
|
+
primaryCredentialKey: "token",
|
|
18216
|
+
requiredCredentialFields: ["token"],
|
|
18217
|
+
requiredOptionFields: ["appId"],
|
|
18218
|
+
build(graph, options) {
|
|
18219
|
+
return createEasConnector(graph, options);
|
|
18220
|
+
},
|
|
18221
|
+
// Runs the connector's real `builds` query at limit 1 — the exact read poll()
|
|
18222
|
+
// performs, minus the pages — so the probe checks both that the EXPO_TOKEN
|
|
18223
|
+
// authenticates and that this app id is reachable, the same probe-the-real-
|
|
18224
|
+
// query discipline Railway and Cloud Run use over a trivial `{ __typename }`.
|
|
18225
|
+
// A bad or wrong-scoped token comes back as an Expo GraphQL error, which
|
|
18226
|
+
// `fetchErroredBuilds` throws on, so it fails honestly here rather than
|
|
18227
|
+
// silently at the first poll.
|
|
18228
|
+
async validate({ credentials, options, fetchImpl }) {
|
|
18229
|
+
const cfg = options;
|
|
18230
|
+
const appId = String(cfg.appId ?? "");
|
|
18231
|
+
if (!appId) return { ok: false, reason: "eas: appId is required to validate" };
|
|
18232
|
+
const probeConfig = {
|
|
18233
|
+
appId,
|
|
18234
|
+
pageSize: 1,
|
|
18235
|
+
maxPages: 1,
|
|
18236
|
+
...cfg.apiUrl ? { apiUrl: cfg.apiUrl } : {}
|
|
18237
|
+
};
|
|
18238
|
+
try {
|
|
18239
|
+
await fetchErroredBuilds(String(credentials.token ?? ""), probeConfig, fetchImpl);
|
|
18240
|
+
return { ok: true };
|
|
18241
|
+
} catch (err) {
|
|
18242
|
+
return { ok: false, reason: `eas auth check failed: ${err.message}` };
|
|
18243
|
+
}
|
|
18244
|
+
}
|
|
17406
18245
|
}
|
|
17407
18246
|
};
|
|
17408
18247
|
function vercelCredsFrom(credentials) {
|
|
@@ -17584,7 +18423,11 @@ async function startConnectorPolling(input) {
|
|
|
17584
18423
|
const stopFns = all.map(
|
|
17585
18424
|
(registration) => startConnectorPollLoop(
|
|
17586
18425
|
registration.connector,
|
|
17587
|
-
{
|
|
18426
|
+
{
|
|
18427
|
+
projectDir: input.projectDir,
|
|
18428
|
+
credentials: registration.credentials,
|
|
18429
|
+
...input.errorsPath ? { errorsPath: input.errorsPath } : {}
|
|
18430
|
+
},
|
|
17588
18431
|
input.graph,
|
|
17589
18432
|
registration.resolveTarget,
|
|
17590
18433
|
{ intervalMs: registration.intervalMs, connectorId: registration.id }
|
|
@@ -17754,11 +18597,11 @@ function registerRoutes(scope, ctx) {
|
|
|
17754
18597
|
const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
17755
18598
|
const parsed = [];
|
|
17756
18599
|
for (const c of candidates) {
|
|
17757
|
-
const r =
|
|
18600
|
+
const r = import_types85.DivergenceTypeSchema.safeParse(c);
|
|
17758
18601
|
if (!r.success) {
|
|
17759
18602
|
return reply.code(400).send({
|
|
17760
18603
|
error: `unknown divergence type "${c}"`,
|
|
17761
|
-
allowed:
|
|
18604
|
+
allowed: import_types85.DivergenceTypeSchema.options
|
|
17762
18605
|
});
|
|
17763
18606
|
}
|
|
17764
18607
|
parsed.push(r.data);
|
|
@@ -17865,10 +18708,15 @@ function registerRoutes(scope, ctx) {
|
|
|
17865
18708
|
}
|
|
17866
18709
|
const reg = built.registration;
|
|
17867
18710
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
18711
|
+
const incidentsPath = errorsPathFor(proj);
|
|
17868
18712
|
try {
|
|
17869
18713
|
const result = await ctx.runPoll(
|
|
17870
18714
|
reg.connector,
|
|
17871
|
-
{
|
|
18715
|
+
{
|
|
18716
|
+
projectDir: proj.scanPath ?? "",
|
|
18717
|
+
credentials: reg.credentials,
|
|
18718
|
+
...incidentsPath ? { errorsPath: incidentsPath } : {}
|
|
18719
|
+
},
|
|
17872
18720
|
proj.graph,
|
|
17873
18721
|
reg.resolveTarget
|
|
17874
18722
|
);
|
|
@@ -17938,6 +18786,34 @@ function registerRoutes(scope, ctx) {
|
|
|
17938
18786
|
}
|
|
17939
18787
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
17940
18788
|
});
|
|
18789
|
+
scope.get("/graph/expand/:nodeId", async (req2, reply) => {
|
|
18790
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
18791
|
+
if (!proj) return;
|
|
18792
|
+
const { nodeId } = req2.params;
|
|
18793
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
18794
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
18795
|
+
}
|
|
18796
|
+
const direction = req2.query.direction;
|
|
18797
|
+
if (direction !== "up" && direction !== "down") {
|
|
18798
|
+
return reply.code(400).send({ error: 'direction must be "up" or "down"' });
|
|
18799
|
+
}
|
|
18800
|
+
const epath = errorsPathFor(proj);
|
|
18801
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
18802
|
+
return expandNode(proj.graph, nodeId, direction, incidents);
|
|
18803
|
+
});
|
|
18804
|
+
scope.get("/graph/relate", async (req2, reply) => {
|
|
18805
|
+
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
18806
|
+
if (!proj) return;
|
|
18807
|
+
const { a, b } = req2.query;
|
|
18808
|
+
if (!a || !b) {
|
|
18809
|
+
return reply.code(400).send({ error: "both a and b query params are required" });
|
|
18810
|
+
}
|
|
18811
|
+
const maxDepth = req2.query.maxDepth ? Number(req2.query.maxDepth) : void 0;
|
|
18812
|
+
if (maxDepth !== void 0 && (!Number.isFinite(maxDepth) || maxDepth < 1)) {
|
|
18813
|
+
return reply.code(400).send({ error: "maxDepth must be a positive integer" });
|
|
18814
|
+
}
|
|
18815
|
+
return relate(proj.graph, a, b, maxDepth);
|
|
18816
|
+
});
|
|
17941
18817
|
scope.get("/search", async (req2, reply) => {
|
|
17942
18818
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
17943
18819
|
if (!proj) return;
|
|
@@ -18067,7 +18943,7 @@ function registerRoutes(scope, ctx) {
|
|
|
18067
18943
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
18068
18944
|
let violations = await log.readAll();
|
|
18069
18945
|
if (req2.query.severity) {
|
|
18070
|
-
const sev =
|
|
18946
|
+
const sev = import_types85.PolicySeveritySchema.safeParse(req2.query.severity);
|
|
18071
18947
|
if (!sev.success) {
|
|
18072
18948
|
return reply.code(400).send({
|
|
18073
18949
|
error: "invalid severity",
|
|
@@ -18106,7 +18982,7 @@ function registerRoutes(scope, ctx) {
|
|
|
18106
18982
|
scope.post("/policies/check", async (req2, reply) => {
|
|
18107
18983
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
18108
18984
|
if (!proj) return;
|
|
18109
|
-
const parsed =
|
|
18985
|
+
const parsed = import_types85.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
|
|
18110
18986
|
if (!parsed.success) {
|
|
18111
18987
|
return reply.code(400).send({
|
|
18112
18988
|
error: "invalid /policies/check body",
|
|
@@ -18447,7 +19323,7 @@ function unroutedErrorsPath(neatHome4) {
|
|
|
18447
19323
|
}
|
|
18448
19324
|
|
|
18449
19325
|
// src/daemon.ts
|
|
18450
|
-
var
|
|
19326
|
+
var import_types86 = require("@neat.is/types");
|
|
18451
19327
|
function daemonJsonPath(scanPath) {
|
|
18452
19328
|
return import_node_path67.default.join(scanPath, "neat-out", "daemon.json");
|
|
18453
19329
|
}
|
|
@@ -18586,7 +19462,7 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
|
|
|
18586
19462
|
if (!serviceName) return true;
|
|
18587
19463
|
if (serviceNameMatchesProject(serviceName, project)) return true;
|
|
18588
19464
|
return graph.someNode(
|
|
18589
|
-
(_id, attrs) => attrs.type ===
|
|
19465
|
+
(_id, attrs) => attrs.type === import_types86.NodeType.ServiceNode && attrs.name === serviceName
|
|
18590
19466
|
);
|
|
18591
19467
|
}
|
|
18592
19468
|
async function bootstrapProject(entry2, connectors = [], neatHome4) {
|
|
@@ -18634,6 +19510,10 @@ async function bootstrapProject(entry2, connectors = [], neatHome4) {
|
|
|
18634
19510
|
project: entry2.name,
|
|
18635
19511
|
graph,
|
|
18636
19512
|
projectDir: entry2.path,
|
|
19513
|
+
// The slot's incident ledger, so an incident-emitting connector (ADR-185)
|
|
19514
|
+
// writes a build-failure incident onto the same errors.ndjson OTLP-derived
|
|
19515
|
+
// incidents land in.
|
|
19516
|
+
errorsPath: paths.errorsPath,
|
|
18637
19517
|
...neatHome4 ? { home: neatHome4 } : {},
|
|
18638
19518
|
extra: connectors,
|
|
18639
19519
|
onSkip: (skipped, reason) => console.warn(
|