@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/index.cjs
CHANGED
|
@@ -1437,7 +1437,7 @@ var rootCauseShapes = {
|
|
|
1437
1437
|
[import_types.NodeType.FileNode]: fileRootCauseShape,
|
|
1438
1438
|
[import_types.NodeType.SymbolNode]: symbolRootCauseShape
|
|
1439
1439
|
};
|
|
1440
|
-
function
|
|
1440
|
+
function legacyRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
1441
1441
|
if (!graph.hasNode(errorNodeId)) return null;
|
|
1442
1442
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
1443
1443
|
const shape = rootCauseShapes[origin.type];
|
|
@@ -1694,7 +1694,9 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
1694
1694
|
dependencies: [],
|
|
1695
1695
|
observed: false,
|
|
1696
1696
|
inboundObservedCount: 0,
|
|
1697
|
-
hasExtractedOutbound: false
|
|
1697
|
+
hasExtractedOutbound: false,
|
|
1698
|
+
inboundVolume: 0,
|
|
1699
|
+
window: "lifetime"
|
|
1698
1700
|
});
|
|
1699
1701
|
}
|
|
1700
1702
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
@@ -1725,11 +1727,19 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
1725
1727
|
}
|
|
1726
1728
|
}
|
|
1727
1729
|
let inboundObservedCount = 0;
|
|
1730
|
+
let inboundVolume = 0;
|
|
1731
|
+
let inboundLastObserved;
|
|
1728
1732
|
for (const tgt of scope) {
|
|
1729
1733
|
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
1730
1734
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1731
1735
|
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1732
|
-
if (e.provenance === import_types.Provenance.OBSERVED)
|
|
1736
|
+
if (e.provenance === import_types.Provenance.OBSERVED) {
|
|
1737
|
+
inboundObservedCount += 1;
|
|
1738
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 1;
|
|
1739
|
+
if (e.lastObserved && (!inboundLastObserved || e.lastObserved > inboundLastObserved)) {
|
|
1740
|
+
inboundLastObserved = e.lastObserved;
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1733
1743
|
}
|
|
1734
1744
|
}
|
|
1735
1745
|
dependencies.sort(
|
|
@@ -1740,7 +1750,291 @@ function getObservedDependencies(graph, nodeId) {
|
|
|
1740
1750
|
dependencies,
|
|
1741
1751
|
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
1742
1752
|
inboundObservedCount,
|
|
1743
|
-
hasExtractedOutbound
|
|
1753
|
+
hasExtractedOutbound,
|
|
1754
|
+
// The signal is cumulative, so the honest window label is "lifetime" (ADR-190).
|
|
1755
|
+
inboundVolume,
|
|
1756
|
+
window: "lifetime",
|
|
1757
|
+
...inboundLastObserved ? { inboundLastObserved } : {}
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
var SATURATION_P95_MS = 1e3;
|
|
1761
|
+
function nodeScope(graph, nodeId) {
|
|
1762
|
+
const scope = [nodeId];
|
|
1763
|
+
if (!graph.hasNode(nodeId)) return scope;
|
|
1764
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
1765
|
+
if (attrs.type === import_types.NodeType.ServiceNode) {
|
|
1766
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
1767
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1768
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1769
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
1770
|
+
if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
return scope;
|
|
1774
|
+
}
|
|
1775
|
+
function incidentCountForNode(nodeId, incidents) {
|
|
1776
|
+
if (!incidents || incidents.length === 0) return 0;
|
|
1777
|
+
return incidents.filter((ev) => incidentMatchesNode(ev, nodeId)).length;
|
|
1778
|
+
}
|
|
1779
|
+
function nodeContext(graph, nodeId, incidents, now = Date.now()) {
|
|
1780
|
+
const scope = nodeScope(graph, nodeId);
|
|
1781
|
+
let errorsFromCallers = 0;
|
|
1782
|
+
let inboundVolume = 0;
|
|
1783
|
+
let outboundVolume = 0;
|
|
1784
|
+
let outboundErrors = 0;
|
|
1785
|
+
let latestInboundMs;
|
|
1786
|
+
let latencyP95Ms;
|
|
1787
|
+
let stale = false;
|
|
1788
|
+
for (const n of scope) {
|
|
1789
|
+
if (!graph.hasNode(n)) continue;
|
|
1790
|
+
for (const edgeId of graph.inboundEdges(n)) {
|
|
1791
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1792
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1793
|
+
errorsFromCallers += e.signal?.errorCount ?? 0;
|
|
1794
|
+
inboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
1795
|
+
if (e.provenance === import_types.Provenance.STALE) stale = true;
|
|
1796
|
+
const p95 = e.signal?.latencyMs?.p95;
|
|
1797
|
+
if (p95 !== void 0) latencyP95Ms = Math.max(latencyP95Ms ?? 0, p95);
|
|
1798
|
+
if (e.lastObserved) {
|
|
1799
|
+
const t = Date.parse(e.lastObserved);
|
|
1800
|
+
if (Number.isFinite(t)) latestInboundMs = Math.max(latestInboundMs ?? 0, t);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
1804
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1805
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1806
|
+
outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
|
|
1807
|
+
if (e.type === import_types.EdgeType.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
|
|
1808
|
+
if (e.provenance === import_types.Provenance.STALE) stale = true;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1811
|
+
const errorsEmittedHere = incidentCountForNode(nodeId, incidents) + outboundErrors;
|
|
1812
|
+
const lastObservedAgeMs = latestInboundMs !== void 0 ? Math.max(0, now - latestInboundMs) : void 0;
|
|
1813
|
+
return {
|
|
1814
|
+
errorsEmittedHere,
|
|
1815
|
+
errorsFromCallers,
|
|
1816
|
+
callCount: inboundVolume,
|
|
1817
|
+
outboundVolume,
|
|
1818
|
+
...lastObservedAgeMs !== void 0 ? { lastObservedAgeMs } : {},
|
|
1819
|
+
...latencyP95Ms !== void 0 ? { latencyP95Ms } : {},
|
|
1820
|
+
stale
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
function isSaturated(ctx) {
|
|
1824
|
+
return ctx.latencyP95Ms !== void 0 && ctx.latencyP95Ms >= SATURATION_P95_MS;
|
|
1825
|
+
}
|
|
1826
|
+
function classifyNode(ctx) {
|
|
1827
|
+
if (ctx.errorsEmittedHere > 0) {
|
|
1828
|
+
if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
|
|
1829
|
+
return "symptom-only";
|
|
1830
|
+
}
|
|
1831
|
+
return "primary-failure";
|
|
1832
|
+
}
|
|
1833
|
+
if (ctx.errorsFromCallers > 0) return "symptom-only";
|
|
1834
|
+
return "unrelated";
|
|
1835
|
+
}
|
|
1836
|
+
function isVictimSeed(ctx) {
|
|
1837
|
+
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
1838
|
+
}
|
|
1839
|
+
function grainOf(graph, nodeId) {
|
|
1840
|
+
if (!graph.hasNode(nodeId)) return "unknown";
|
|
1841
|
+
const t = graph.getNodeAttributes(nodeId).type;
|
|
1842
|
+
if (t === import_types.NodeType.ServiceNode) return "service";
|
|
1843
|
+
if (t === import_types.NodeType.FileNode) return "file";
|
|
1844
|
+
if (t === import_types.NodeType.SymbolNode) return "symbol";
|
|
1845
|
+
return t;
|
|
1846
|
+
}
|
|
1847
|
+
function findPath(graph, from, to, direction, maxDepth) {
|
|
1848
|
+
if (!graph.hasNode(from) || !graph.hasNode(to)) return null;
|
|
1849
|
+
if (from === to) return { nodes: [from], edges: [] };
|
|
1850
|
+
const queue = [{ nodeId: from, depth: 0, nodes: [from], edges: [] }];
|
|
1851
|
+
const enqueued = /* @__PURE__ */ new Set([from]);
|
|
1852
|
+
while (queue.length > 0) {
|
|
1853
|
+
const frame = queue.shift();
|
|
1854
|
+
if (frame.depth >= maxDepth) continue;
|
|
1855
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
|
|
1856
|
+
const neighbours = [...best.entries()].sort((a, b) => a[0].localeCompare(b[0]));
|
|
1857
|
+
for (const [nid, edge] of neighbours) {
|
|
1858
|
+
if (nid === to) return { nodes: [...frame.nodes, nid], edges: [...frame.edges, edge] };
|
|
1859
|
+
if (enqueued.has(nid)) continue;
|
|
1860
|
+
enqueued.add(nid);
|
|
1861
|
+
queue.push({
|
|
1862
|
+
nodeId: nid,
|
|
1863
|
+
depth: frame.depth + 1,
|
|
1864
|
+
nodes: [...frame.nodes, nid],
|
|
1865
|
+
edges: [...frame.edges, edge]
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
return null;
|
|
1870
|
+
}
|
|
1871
|
+
var EMPTY_CONTEXT = {
|
|
1872
|
+
errorsEmittedHere: 0,
|
|
1873
|
+
errorsFromCallers: 0,
|
|
1874
|
+
callCount: 0,
|
|
1875
|
+
outboundVolume: 0,
|
|
1876
|
+
stale: false
|
|
1877
|
+
};
|
|
1878
|
+
function expandNode(graph, nodeId, direction, incidents, now = Date.now()) {
|
|
1879
|
+
if (!graph.hasNode(nodeId)) {
|
|
1880
|
+
return import_types.ExpandResultSchema.parse({
|
|
1881
|
+
origin: nodeId,
|
|
1882
|
+
direction,
|
|
1883
|
+
node: { id: nodeId, classification: "unrelated", context: EMPTY_CONTEXT },
|
|
1884
|
+
neighbours: []
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
const ctx = nodeContext(graph, nodeId, incidents, now);
|
|
1888
|
+
const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(nodeId));
|
|
1889
|
+
const neighbours = [];
|
|
1890
|
+
for (const [nid, edge] of best) {
|
|
1891
|
+
if (edge.type === import_types.EdgeType.CONTAINS) continue;
|
|
1892
|
+
const nctx = nodeContext(graph, nid, incidents, now);
|
|
1893
|
+
neighbours.push({
|
|
1894
|
+
node: nid,
|
|
1895
|
+
edgeType: edge.type,
|
|
1896
|
+
provenance: edge.provenance,
|
|
1897
|
+
classification: classifyNode(nctx),
|
|
1898
|
+
context: nctx
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
neighbours.sort((a, b) => a.node.localeCompare(b.node));
|
|
1902
|
+
return import_types.ExpandResultSchema.parse({
|
|
1903
|
+
origin: nodeId,
|
|
1904
|
+
direction,
|
|
1905
|
+
node: { id: nodeId, classification: classifyNode(ctx), context: ctx },
|
|
1906
|
+
neighbours
|
|
1907
|
+
});
|
|
1908
|
+
}
|
|
1909
|
+
function relate(graph, a, b, maxDepth = ROOT_CAUSE_MAX_DEPTH) {
|
|
1910
|
+
const buildPath = (fp) => ({
|
|
1911
|
+
nodes: fp.nodes,
|
|
1912
|
+
edgeTypes: fp.edges.map((e) => e.type),
|
|
1913
|
+
provenance: fp.edges.map((e) => e.provenance),
|
|
1914
|
+
grain: fp.nodes.map((n) => grainOf(graph, n)),
|
|
1915
|
+
// The failure runs end to end when every hop carries error / latency / alert
|
|
1916
|
+
// signal — that is what turns reachability into cause-confirmation.
|
|
1917
|
+
carriesSignal: fp.edges.length > 0 && fp.edges.every(
|
|
1918
|
+
(e) => (e.signal?.errorCount ?? 0) > 0 || e.signal?.latencyMs !== void 0 || e.signal?.anomalous !== void 0
|
|
1919
|
+
)
|
|
1920
|
+
});
|
|
1921
|
+
if (!graph.hasNode(a) || !graph.hasNode(b)) {
|
|
1922
|
+
return import_types.RelateResultSchema.parse({
|
|
1923
|
+
a,
|
|
1924
|
+
b,
|
|
1925
|
+
related: false,
|
|
1926
|
+
direction: null,
|
|
1927
|
+
paths: [],
|
|
1928
|
+
note: !graph.hasNode(a) ? `node not found: ${a}` : `node not found: ${b}`
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
const down = findPath(graph, a, b, "down", maxDepth);
|
|
1932
|
+
const up = findPath(graph, a, b, "up", maxDepth);
|
|
1933
|
+
if (!down && !up) {
|
|
1934
|
+
return import_types.RelateResultSchema.parse({
|
|
1935
|
+
a,
|
|
1936
|
+
b,
|
|
1937
|
+
related: false,
|
|
1938
|
+
direction: null,
|
|
1939
|
+
paths: [],
|
|
1940
|
+
note: `no path within ${maxDepth} hops`
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
const paths = [];
|
|
1944
|
+
const direction = down ? "a->b" : "b->a";
|
|
1945
|
+
if (down) paths.push(buildPath(down));
|
|
1946
|
+
if (up) paths.push(buildPath(up));
|
|
1947
|
+
const endpointsFine = grainOf(graph, a) !== "service" && grainOf(graph, b) !== "service";
|
|
1948
|
+
const grainGap = endpointsFine && paths[0].grain.some((g) => g === "service");
|
|
1949
|
+
return import_types.RelateResultSchema.parse({
|
|
1950
|
+
a,
|
|
1951
|
+
b,
|
|
1952
|
+
related: true,
|
|
1953
|
+
direction,
|
|
1954
|
+
paths,
|
|
1955
|
+
...grainGap ? { grainGap: true } : {}
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
function findLoadOrigin(graph, alertNodeId, incidents, now) {
|
|
1959
|
+
const upstream = getBlastRadius(graph, alertNodeId).affectedNodes.map((n) => n.nodeId);
|
|
1960
|
+
let best = null;
|
|
1961
|
+
for (const nid of upstream) {
|
|
1962
|
+
if (nid === alertNodeId) continue;
|
|
1963
|
+
const ctx = nodeContext(graph, nid, incidents, now);
|
|
1964
|
+
if (ctx.outboundVolume === 0) continue;
|
|
1965
|
+
const isSource = ctx.callCount === 0;
|
|
1966
|
+
const better = !best || (isSource !== best.isSource ? isSource : ctx.outboundVolume !== best.ctx.outboundVolume ? ctx.outboundVolume > best.ctx.outboundVolume : nid < best.node);
|
|
1967
|
+
if (better) best = { node: nid, ctx, isSource };
|
|
1968
|
+
}
|
|
1969
|
+
return best ? { node: best.node, ctx: best.ctx } : null;
|
|
1970
|
+
}
|
|
1971
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents, opts) {
|
|
1972
|
+
const legacy = legacyRootCause(graph, errorNodeId, errorEvent, incidents);
|
|
1973
|
+
if (!legacy) return null;
|
|
1974
|
+
const navigation = opts?.navigation ?? process.env.NEAT_RCA_NAVIGATION !== "0";
|
|
1975
|
+
if (!navigation) return legacy;
|
|
1976
|
+
return enrichWithNavigation(graph, errorNodeId, legacy, incidents, opts?.now ?? Date.now());
|
|
1977
|
+
}
|
|
1978
|
+
function enrichWithNavigation(graph, errorNodeId, legacy, incidents, now) {
|
|
1979
|
+
const seedNode = legacy.rootCauseNode;
|
|
1980
|
+
const seedCtx = graph.hasNode(seedNode) ? nodeContext(graph, seedNode, incidents, now) : null;
|
|
1981
|
+
const lastProv = legacy.edgeProvenances[legacy.edgeProvenances.length - 1];
|
|
1982
|
+
const candidates = [];
|
|
1983
|
+
if (seedCtx && isVictimSeed(seedCtx)) {
|
|
1984
|
+
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
1985
|
+
if (origin) {
|
|
1986
|
+
const path68 = findPath(graph, errorNodeId, origin.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
1987
|
+
const originConfidence = confidenceFromMix(path68?.edges ?? [], now);
|
|
1988
|
+
candidates.push({
|
|
1989
|
+
node: origin.node,
|
|
1990
|
+
classification: "primary-failure",
|
|
1991
|
+
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.`,
|
|
1992
|
+
context: origin.ctx,
|
|
1993
|
+
confidence: Math.max(0.3, Math.min(0.8, originConfidence || 0.5)),
|
|
1994
|
+
provenance: import_types.Provenance.OBSERVED
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
const staleNote = seedCtx.stale ? "; the node has gone STALE" : "";
|
|
1998
|
+
const satNote = isSaturated(seedCtx) ? `; inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
1999
|
+
candidates.push({
|
|
2000
|
+
node: seedNode,
|
|
2001
|
+
classification: "symptom-only",
|
|
2002
|
+
reason: `Errors arrive from callers (${seedCtx.errorsFromCallers}) but none originate here${staleNote}${satNote} \u2014 a downstream victim of load, not the fault.`,
|
|
2003
|
+
context: seedCtx,
|
|
2004
|
+
confidence: Math.min(legacy.confidence, 0.4),
|
|
2005
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
2006
|
+
});
|
|
2007
|
+
} else {
|
|
2008
|
+
candidates.push({
|
|
2009
|
+
node: seedNode,
|
|
2010
|
+
classification: "primary-failure",
|
|
2011
|
+
reason: legacy.rootCauseReason,
|
|
2012
|
+
context: seedCtx ?? EMPTY_CONTEXT,
|
|
2013
|
+
confidence: legacy.confidence,
|
|
2014
|
+
...lastProv ? { provenance: lastProv } : {}
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
const top = candidates[0];
|
|
2018
|
+
let traversalPath = legacy.traversalPath;
|
|
2019
|
+
let edgeProvenances = legacy.edgeProvenances;
|
|
2020
|
+
if (top.node !== seedNode) {
|
|
2021
|
+
const path68 = findPath(graph, errorNodeId, top.node, "up", ROOT_CAUSE_MAX_DEPTH);
|
|
2022
|
+
if (path68) {
|
|
2023
|
+
traversalPath = path68.nodes;
|
|
2024
|
+
edgeProvenances = path68.edges.map((e) => e.provenance);
|
|
2025
|
+
} else {
|
|
2026
|
+
traversalPath = [errorNodeId, top.node];
|
|
2027
|
+
edgeProvenances = [top.provenance ?? import_types.Provenance.OBSERVED];
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
return import_types.RootCauseResultSchema.parse({
|
|
2031
|
+
rootCauseNode: top.node,
|
|
2032
|
+
rootCauseReason: top.reason,
|
|
2033
|
+
traversalPath,
|
|
2034
|
+
edgeProvenances,
|
|
2035
|
+
confidence: top.confidence,
|
|
2036
|
+
...legacy.fixRecommendation ? { fixRecommendation: legacy.fixRecommendation } : {},
|
|
2037
|
+
candidates
|
|
1744
2038
|
});
|
|
1745
2039
|
}
|
|
1746
2040
|
|
|
@@ -2266,6 +2560,7 @@ var import_yaml = require("yaml");
|
|
|
2266
2560
|
var import_types3 = require("@neat.is/types");
|
|
2267
2561
|
var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go", ".rb", ".php"]);
|
|
2268
2562
|
var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
|
|
2563
|
+
var JSON_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["app.json", "app.config.json", "eas.json"]);
|
|
2269
2564
|
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
2270
2565
|
"node_modules",
|
|
2271
2566
|
".git",
|
|
@@ -2305,6 +2600,7 @@ async function isPythonVenvDir(dir) {
|
|
|
2305
2600
|
function isConfigFile(name) {
|
|
2306
2601
|
const ext = import_node_path3.default.extname(name);
|
|
2307
2602
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
2603
|
+
if (JSON_CONFIG_FILENAMES.has(name)) return { match: true, fileType: "json" };
|
|
2308
2604
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
2309
2605
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
2310
2606
|
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
@@ -4621,6 +4917,63 @@ function columnIsObserved(col) {
|
|
|
4621
4917
|
return col.provenances.includes(import_types7.Provenance.OBSERVED);
|
|
4622
4918
|
}
|
|
4623
4919
|
|
|
4920
|
+
// src/latency-digest.ts
|
|
4921
|
+
init_cjs_shims();
|
|
4922
|
+
var SUB = 16;
|
|
4923
|
+
var MIN_EXP = -10;
|
|
4924
|
+
var MAX_EXP = 22;
|
|
4925
|
+
var OCTAVES = MAX_EXP - MIN_EXP + 1;
|
|
4926
|
+
var OVERFLOW_INDEX = OCTAVES * SUB + 1;
|
|
4927
|
+
function latencyBucketIndex(ms) {
|
|
4928
|
+
if (!Number.isFinite(ms) || ms <= 0) return 0;
|
|
4929
|
+
const e = Math.floor(Math.log2(ms));
|
|
4930
|
+
if (e < MIN_EXP) return 0;
|
|
4931
|
+
if (e > MAX_EXP) return OVERFLOW_INDEX;
|
|
4932
|
+
const base = 2 ** e;
|
|
4933
|
+
const raw = Math.floor((ms / base - 1) * SUB);
|
|
4934
|
+
const s = raw < 0 ? 0 : raw >= SUB ? SUB - 1 : raw;
|
|
4935
|
+
return (e - MIN_EXP) * SUB + s + 1;
|
|
4936
|
+
}
|
|
4937
|
+
function bucketRepresentativeMs(index) {
|
|
4938
|
+
if (index <= 0) return 0;
|
|
4939
|
+
if (index >= OVERFLOW_INDEX) return 2 ** (MAX_EXP + 1);
|
|
4940
|
+
const zeroBased = index - 1;
|
|
4941
|
+
const e = MIN_EXP + Math.floor(zeroBased / SUB);
|
|
4942
|
+
const s = zeroBased % SUB;
|
|
4943
|
+
const base = 2 ** e;
|
|
4944
|
+
const lower = base * (1 + s / SUB);
|
|
4945
|
+
const upper = base * (1 + (s + 1) / SUB);
|
|
4946
|
+
return (lower + upper) / 2;
|
|
4947
|
+
}
|
|
4948
|
+
function recordLatency(hist, ms) {
|
|
4949
|
+
const key = String(latencyBucketIndex(ms));
|
|
4950
|
+
hist[key] = (hist[key] ?? 0) + 1;
|
|
4951
|
+
return hist;
|
|
4952
|
+
}
|
|
4953
|
+
function quantile(hist, q) {
|
|
4954
|
+
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]);
|
|
4955
|
+
let total = 0;
|
|
4956
|
+
for (const [, c] of entries) total += c;
|
|
4957
|
+
if (total === 0) return 0;
|
|
4958
|
+
const rank = Math.max(1, Math.ceil(q * total));
|
|
4959
|
+
let cumulative = 0;
|
|
4960
|
+
for (const [idx, c] of entries) {
|
|
4961
|
+
cumulative += c;
|
|
4962
|
+
if (cumulative >= rank) return bucketRepresentativeMs(idx);
|
|
4963
|
+
}
|
|
4964
|
+
return bucketRepresentativeMs(entries[entries.length - 1][0]);
|
|
4965
|
+
}
|
|
4966
|
+
function round(ms) {
|
|
4967
|
+
return Math.round(ms * 100) / 100;
|
|
4968
|
+
}
|
|
4969
|
+
function latencyPercentiles(hist) {
|
|
4970
|
+
if (!hist) return void 0;
|
|
4971
|
+
let total = 0;
|
|
4972
|
+
for (const c of Object.values(hist)) total += c;
|
|
4973
|
+
if (total === 0) return void 0;
|
|
4974
|
+
return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
|
|
4975
|
+
}
|
|
4976
|
+
|
|
4624
4977
|
// src/ingest.ts
|
|
4625
4978
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
4626
4979
|
var DAY_MS = 24 * HOUR_MS;
|
|
@@ -5316,7 +5669,7 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
5316
5669
|
graph.addNode(id, node);
|
|
5317
5670
|
return id;
|
|
5318
5671
|
}
|
|
5319
|
-
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
|
|
5672
|
+
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence, durationMs) {
|
|
5320
5673
|
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
5321
5674
|
const grain = source.startsWith("file:") || source.startsWith("symbol:") ? "file" : "service";
|
|
5322
5675
|
const id = makeObservedEdgeId(type, source, target);
|
|
@@ -5324,10 +5677,15 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
5324
5677
|
const existing = graph.getEdgeAttributes(id);
|
|
5325
5678
|
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
5326
5679
|
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
5680
|
+
const latencyHist2 = durationMs !== void 0 ? recordLatency({ ...existing.signal?.latencyHist ?? {} }, durationMs) : existing.signal?.latencyHist;
|
|
5681
|
+
const latencyMs2 = latencyPercentiles(latencyHist2) ?? existing.signal?.latencyMs;
|
|
5327
5682
|
const newSignal = {
|
|
5328
5683
|
spanCount: newSpanCount,
|
|
5329
5684
|
errorCount: newErrorCount,
|
|
5330
|
-
lastObservedAgeMs: 0
|
|
5685
|
+
lastObservedAgeMs: 0,
|
|
5686
|
+
...latencyHist2 ? { latencyHist: latencyHist2 } : {},
|
|
5687
|
+
...latencyMs2 ? { latencyMs: latencyMs2 } : {},
|
|
5688
|
+
...existing.signal?.anomalous !== void 0 ? { anomalous: existing.signal.anomalous } : {}
|
|
5331
5689
|
};
|
|
5332
5690
|
const updated = {
|
|
5333
5691
|
...existing,
|
|
@@ -5342,10 +5700,14 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
5342
5700
|
graph.replaceEdgeAttributes(id, updated);
|
|
5343
5701
|
return { edge: updated, created: false };
|
|
5344
5702
|
}
|
|
5703
|
+
const latencyHist = durationMs !== void 0 ? recordLatency({}, durationMs) : void 0;
|
|
5704
|
+
const latencyMs = latencyHist ? latencyPercentiles(latencyHist) : void 0;
|
|
5345
5705
|
const signal = {
|
|
5346
5706
|
spanCount: 1,
|
|
5347
5707
|
errorCount: isError ? 1 : 0,
|
|
5348
|
-
lastObservedAgeMs: 0
|
|
5708
|
+
lastObservedAgeMs: 0,
|
|
5709
|
+
...latencyHist ? { latencyHist } : {},
|
|
5710
|
+
...latencyMs ? { latencyMs } : {}
|
|
5349
5711
|
};
|
|
5350
5712
|
const edge = {
|
|
5351
5713
|
id,
|
|
@@ -5409,6 +5771,21 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
5409
5771
|
await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(ctx.errorsPath), { recursive: true });
|
|
5410
5772
|
await import_node_fs7.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
5411
5773
|
}
|
|
5774
|
+
async function appendConnectorIncident(errorsPath, input) {
|
|
5775
|
+
const ev = {
|
|
5776
|
+
id: input.id,
|
|
5777
|
+
timestamp: input.timestamp,
|
|
5778
|
+
service: input.service,
|
|
5779
|
+
traceId: input.id,
|
|
5780
|
+
spanId: input.id,
|
|
5781
|
+
errorType: input.errorType,
|
|
5782
|
+
errorMessage: input.errorMessage,
|
|
5783
|
+
...input.attributes && Object.keys(input.attributes).length > 0 ? { attributes: input.attributes } : {},
|
|
5784
|
+
affectedNode: input.affectedNode
|
|
5785
|
+
};
|
|
5786
|
+
await import_node_fs7.promises.mkdir(import_node_path8.default.dirname(errorsPath), { recursive: true });
|
|
5787
|
+
await import_node_fs7.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
5788
|
+
}
|
|
5412
5789
|
function incidentAffectedNode(span, graph, scanPath) {
|
|
5413
5790
|
const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
|
|
5414
5791
|
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
@@ -5555,6 +5932,7 @@ async function handleSpan(ctx, span) {
|
|
|
5555
5932
|
}
|
|
5556
5933
|
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
5557
5934
|
const isError = span.statusCode === 2;
|
|
5935
|
+
const durationMs = span.durationNanos > 0n ? Number(span.durationNanos) / 1e6 : void 0;
|
|
5558
5936
|
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
5559
5937
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
5560
5938
|
cacheSpanService(span, nowMs, callSite);
|
|
@@ -5593,7 +5971,8 @@ async function handleSpan(ctx, span) {
|
|
|
5593
5971
|
targetId,
|
|
5594
5972
|
ts,
|
|
5595
5973
|
isError,
|
|
5596
|
-
callSiteEvidence
|
|
5974
|
+
callSiteEvidence,
|
|
5975
|
+
durationMs
|
|
5597
5976
|
);
|
|
5598
5977
|
if (result) affectedNode = targetId;
|
|
5599
5978
|
if (span.dbSystem === "mongodb" && span.dbCollection) {
|
|
@@ -5605,7 +5984,8 @@ async function handleSpan(ctx, span) {
|
|
|
5605
5984
|
collectionId,
|
|
5606
5985
|
ts,
|
|
5607
5986
|
isError,
|
|
5608
|
-
callSiteEvidence
|
|
5987
|
+
callSiteEvidence,
|
|
5988
|
+
durationMs
|
|
5609
5989
|
);
|
|
5610
5990
|
}
|
|
5611
5991
|
if (span.dbTable) {
|
|
@@ -5617,7 +5997,8 @@ async function handleSpan(ctx, span) {
|
|
|
5617
5997
|
tableId,
|
|
5618
5998
|
ts,
|
|
5619
5999
|
isError,
|
|
5620
|
-
callSiteEvidence
|
|
6000
|
+
callSiteEvidence,
|
|
6001
|
+
durationMs
|
|
5621
6002
|
);
|
|
5622
6003
|
mergeObservedColumns(ctx.graph, tableId, span.dbColumns);
|
|
5623
6004
|
}
|
|
@@ -5636,7 +6017,8 @@ async function handleSpan(ctx, span) {
|
|
|
5636
6017
|
targetId,
|
|
5637
6018
|
ts,
|
|
5638
6019
|
isError,
|
|
5639
|
-
callSiteEvidence
|
|
6020
|
+
callSiteEvidence,
|
|
6021
|
+
durationMs
|
|
5640
6022
|
);
|
|
5641
6023
|
if (result) affectedNode = targetId;
|
|
5642
6024
|
} else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
|
|
@@ -5653,7 +6035,8 @@ async function handleSpan(ctx, span) {
|
|
|
5653
6035
|
targetId,
|
|
5654
6036
|
ts,
|
|
5655
6037
|
isError,
|
|
5656
|
-
callSiteEvidence
|
|
6038
|
+
callSiteEvidence,
|
|
6039
|
+
durationMs
|
|
5657
6040
|
);
|
|
5658
6041
|
if (result) affectedNode = targetId;
|
|
5659
6042
|
} else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
|
|
@@ -5665,7 +6048,8 @@ async function handleSpan(ctx, span) {
|
|
|
5665
6048
|
targetId,
|
|
5666
6049
|
ts,
|
|
5667
6050
|
isError,
|
|
5668
|
-
callSiteEvidence
|
|
6051
|
+
callSiteEvidence,
|
|
6052
|
+
durationMs
|
|
5669
6053
|
);
|
|
5670
6054
|
if (result) affectedNode = targetId;
|
|
5671
6055
|
} else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
|
|
@@ -5681,7 +6065,8 @@ async function handleSpan(ctx, span) {
|
|
|
5681
6065
|
targetId,
|
|
5682
6066
|
ts,
|
|
5683
6067
|
isError,
|
|
5684
|
-
callSiteEvidence
|
|
6068
|
+
callSiteEvidence,
|
|
6069
|
+
durationMs
|
|
5685
6070
|
);
|
|
5686
6071
|
if (result) affectedNode = targetId;
|
|
5687
6072
|
} else {
|
|
@@ -5697,7 +6082,8 @@ async function handleSpan(ctx, span) {
|
|
|
5697
6082
|
targetId,
|
|
5698
6083
|
ts,
|
|
5699
6084
|
isError,
|
|
5700
|
-
callSiteEvidence
|
|
6085
|
+
callSiteEvidence,
|
|
6086
|
+
durationMs
|
|
5701
6087
|
);
|
|
5702
6088
|
affectedNode = targetId;
|
|
5703
6089
|
resolvedViaAddress = true;
|
|
@@ -5710,7 +6096,8 @@ async function handleSpan(ctx, span) {
|
|
|
5710
6096
|
frontierNodeId,
|
|
5711
6097
|
ts,
|
|
5712
6098
|
isError,
|
|
5713
|
-
callSiteEvidence
|
|
6099
|
+
callSiteEvidence,
|
|
6100
|
+
durationMs
|
|
5714
6101
|
);
|
|
5715
6102
|
affectedNode = frontierNodeId;
|
|
5716
6103
|
resolvedViaAddress = true;
|
|
@@ -5736,7 +6123,8 @@ async function handleSpan(ctx, span) {
|
|
|
5736
6123
|
sourceId,
|
|
5737
6124
|
ts,
|
|
5738
6125
|
isError,
|
|
5739
|
-
fallbackEvidence
|
|
6126
|
+
fallbackEvidence,
|
|
6127
|
+
durationMs
|
|
5740
6128
|
);
|
|
5741
6129
|
}
|
|
5742
6130
|
}
|
|
@@ -5750,7 +6138,7 @@ async function handleSpan(ctx, span) {
|
|
|
5750
6138
|
);
|
|
5751
6139
|
if (routeNodeId) {
|
|
5752
6140
|
const routeSvc = ctx.graph.getNodeAttributes(routeNodeId).service;
|
|
5753
|
-
upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError);
|
|
6141
|
+
upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError, void 0, durationMs);
|
|
5754
6142
|
}
|
|
5755
6143
|
}
|
|
5756
6144
|
if (span.statusCode === 2) {
|
|
@@ -11222,8 +11610,41 @@ var import_tree_sitter14 = __toESM(require("tree-sitter"), 1);
|
|
|
11222
11610
|
var import_tree_sitter_go3 = __toESM(require("tree-sitter-go"), 1);
|
|
11223
11611
|
var import_types35 = require("@neat.is/types");
|
|
11224
11612
|
init_otel();
|
|
11225
|
-
var SQL_METHODS = /* @__PURE__ */ new Set(["Exec", "ExecContext", "Query", "QueryContext", "QueryRow", "QueryRowContext"]);
|
|
11226
11613
|
var PARSE_CHUNK10 = 16384;
|
|
11614
|
+
var DATABASE_SQL_METHODS = /* @__PURE__ */ new Set([
|
|
11615
|
+
"Query",
|
|
11616
|
+
"QueryContext",
|
|
11617
|
+
"QueryRow",
|
|
11618
|
+
"QueryRowContext",
|
|
11619
|
+
"Exec",
|
|
11620
|
+
"ExecContext",
|
|
11621
|
+
"Prepare",
|
|
11622
|
+
"PrepareContext"
|
|
11623
|
+
]);
|
|
11624
|
+
var SQLX_METHODS = /* @__PURE__ */ new Set([
|
|
11625
|
+
"Get",
|
|
11626
|
+
"Select",
|
|
11627
|
+
"Queryx",
|
|
11628
|
+
"QueryRowx",
|
|
11629
|
+
"NamedExec",
|
|
11630
|
+
"NamedQuery",
|
|
11631
|
+
"MustExec",
|
|
11632
|
+
"Preparex",
|
|
11633
|
+
"GetContext",
|
|
11634
|
+
"SelectContext"
|
|
11635
|
+
]);
|
|
11636
|
+
var DATABASE_SQL_IMPORT = "database/sql";
|
|
11637
|
+
var SQLX_IMPORT = "github.com/jmoiron/sqlx";
|
|
11638
|
+
function makeGoParser3() {
|
|
11639
|
+
const p = new import_tree_sitter14.default();
|
|
11640
|
+
p.setLanguage(import_tree_sitter_go3.default);
|
|
11641
|
+
return p;
|
|
11642
|
+
}
|
|
11643
|
+
function parseSource10(parser, source) {
|
|
11644
|
+
return parser.parse(
|
|
11645
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK10)
|
|
11646
|
+
);
|
|
11647
|
+
}
|
|
11227
11648
|
function walk7(node, visit) {
|
|
11228
11649
|
visit(node);
|
|
11229
11650
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -11231,25 +11652,54 @@ function walk7(node, visit) {
|
|
|
11231
11652
|
if (child) walk7(child, visit);
|
|
11232
11653
|
}
|
|
11233
11654
|
}
|
|
11655
|
+
function goStringLiteralValue(node) {
|
|
11656
|
+
if (!node) return null;
|
|
11657
|
+
if (node.type === "interpreted_string_literal" || node.type === "raw_string_literal") {
|
|
11658
|
+
const t = node.text;
|
|
11659
|
+
return t.length >= 2 ? t.slice(1, -1) : "";
|
|
11660
|
+
}
|
|
11661
|
+
return null;
|
|
11662
|
+
}
|
|
11663
|
+
function goImportsAny(root, names) {
|
|
11664
|
+
let found = false;
|
|
11665
|
+
walk7(root, (node) => {
|
|
11666
|
+
if (found || node.type !== "import_spec") return;
|
|
11667
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
11668
|
+
const value = goStringLiteralValue(node.namedChild(i));
|
|
11669
|
+
if (value !== null && names.has(value)) found = true;
|
|
11670
|
+
}
|
|
11671
|
+
});
|
|
11672
|
+
return found;
|
|
11673
|
+
}
|
|
11674
|
+
function firstStringLiteralArg(argsNode) {
|
|
11675
|
+
if (!argsNode) return null;
|
|
11676
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
11677
|
+
const value = goStringLiteralValue(argsNode.namedChild(i));
|
|
11678
|
+
if (value !== null) return value;
|
|
11679
|
+
}
|
|
11680
|
+
return null;
|
|
11681
|
+
}
|
|
11234
11682
|
function goSqlEndpointsFromFile(file, serviceDir) {
|
|
11235
11683
|
if (import_node_path47.default.extname(file.path) !== ".go") return [];
|
|
11236
|
-
|
|
11237
|
-
|
|
11238
|
-
const
|
|
11239
|
-
|
|
11240
|
-
);
|
|
11684
|
+
if (!file.content.includes(DATABASE_SQL_IMPORT) && !file.content.includes(SQLX_IMPORT)) return [];
|
|
11685
|
+
const tree = parseSource10(makeGoParser3(), file.content);
|
|
11686
|
+
const importsDatabaseSql = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([DATABASE_SQL_IMPORT]));
|
|
11687
|
+
const importsSqlx = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([SQLX_IMPORT]));
|
|
11688
|
+
if (!importsDatabaseSql && !importsSqlx) return [];
|
|
11241
11689
|
const out = [];
|
|
11242
11690
|
walk7(tree.rootNode, (node) => {
|
|
11243
11691
|
if (node.type !== "call_expression") return;
|
|
11244
11692
|
const fn = node.childForFieldName("function");
|
|
11245
11693
|
if (fn?.type !== "selector_expression") return;
|
|
11246
11694
|
const method = fn.childForFieldName("field")?.text;
|
|
11247
|
-
if (!method
|
|
11248
|
-
const
|
|
11249
|
-
if (!
|
|
11250
|
-
const sql =
|
|
11695
|
+
if (!method) return;
|
|
11696
|
+
const recognized = DATABASE_SQL_METHODS.has(method) || importsSqlx && SQLX_METHODS.has(method);
|
|
11697
|
+
if (!recognized) return;
|
|
11698
|
+
const sql = firstStringLiteralArg(node.childForFieldName("arguments"));
|
|
11699
|
+
if (sql === null) return;
|
|
11251
11700
|
const table = tableFromSqlStatement(sql);
|
|
11252
11701
|
if (!table) return;
|
|
11702
|
+
const columns = columnsFromSqlStatement(sql);
|
|
11253
11703
|
const line = node.startPosition.row + 1;
|
|
11254
11704
|
out.push({
|
|
11255
11705
|
infraId: (0, import_types35.infraId)("sql-table", table),
|
|
@@ -11257,7 +11707,12 @@ function goSqlEndpointsFromFile(file, serviceDir) {
|
|
|
11257
11707
|
kind: "sql-table",
|
|
11258
11708
|
edgeType: "CALLS",
|
|
11259
11709
|
confidenceKind: "verified-call-site",
|
|
11260
|
-
|
|
11710
|
+
...columns.length > 0 ? { columns } : {},
|
|
11711
|
+
evidence: {
|
|
11712
|
+
file: toPosix(import_node_path47.default.relative(serviceDir, file.path)),
|
|
11713
|
+
line,
|
|
11714
|
+
snippet: snippet(file.content, line)
|
|
11715
|
+
}
|
|
11261
11716
|
});
|
|
11262
11717
|
});
|
|
11263
11718
|
return out;
|
|
@@ -11271,12 +11726,12 @@ var import_tree_sitter_go4 = __toESM(require("tree-sitter-go"), 1);
|
|
|
11271
11726
|
var import_types36 = require("@neat.is/types");
|
|
11272
11727
|
var GORM_IMPORT_RE = /gorm\.io\/gorm/;
|
|
11273
11728
|
var PARSE_CHUNK11 = 16384;
|
|
11274
|
-
function
|
|
11729
|
+
function makeGoParser4() {
|
|
11275
11730
|
const p = new import_tree_sitter15.default();
|
|
11276
11731
|
p.setLanguage(import_tree_sitter_go4.default);
|
|
11277
11732
|
return p;
|
|
11278
11733
|
}
|
|
11279
|
-
function
|
|
11734
|
+
function parseSource11(parser, source) {
|
|
11280
11735
|
return parser.parse(
|
|
11281
11736
|
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK11)
|
|
11282
11737
|
);
|
|
@@ -11700,7 +12155,7 @@ function collectColumns(struct, structs, seen, prefix, out, emitted) {
|
|
|
11700
12155
|
function gormEndpointsFromFile(file, serviceDir) {
|
|
11701
12156
|
if (import_node_path48.default.extname(file.path) !== ".go") return [];
|
|
11702
12157
|
if (!GORM_IMPORT_RE.test(file.content)) return [];
|
|
11703
|
-
const tree =
|
|
12158
|
+
const tree = parseSource11(makeGoParser4(), file.content);
|
|
11704
12159
|
const { structs, models, tableFor } = analyze(tree);
|
|
11705
12160
|
const out = [];
|
|
11706
12161
|
const seenTables = /* @__PURE__ */ new Set();
|
|
@@ -11731,7 +12186,7 @@ function gormEndpointsFromFile(file, serviceDir) {
|
|
|
11731
12186
|
function gormForeignKeys(file, serviceDir) {
|
|
11732
12187
|
if (import_node_path48.default.extname(file.path) !== ".go") return [];
|
|
11733
12188
|
if (!GORM_IMPORT_RE.test(file.content)) return [];
|
|
11734
|
-
const tree =
|
|
12189
|
+
const tree = parseSource11(makeGoParser4(), file.content);
|
|
11735
12190
|
const { structs, models, tableFor } = analyze(tree);
|
|
11736
12191
|
const out = [];
|
|
11737
12192
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -13474,7 +13929,7 @@ function startPersistLoop(graph, outPath, opts = {}) {
|
|
|
13474
13929
|
init_cjs_shims();
|
|
13475
13930
|
var import_fastify2 = __toESM(require("fastify"), 1);
|
|
13476
13931
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
13477
|
-
var
|
|
13932
|
+
var import_types85 = require("@neat.is/types");
|
|
13478
13933
|
|
|
13479
13934
|
// src/extend/index.ts
|
|
13480
13935
|
init_cjs_shims();
|
|
@@ -14980,6 +15435,23 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
|
|
|
14980
15435
|
unresolved++;
|
|
14981
15436
|
continue;
|
|
14982
15437
|
}
|
|
15438
|
+
if (signal.incident) {
|
|
15439
|
+
ensureServiceNode(graph, resolved.serviceName, NO_ENV);
|
|
15440
|
+
if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
|
|
15441
|
+
unresolved++;
|
|
15442
|
+
continue;
|
|
15443
|
+
}
|
|
15444
|
+
await appendConnectorIncident(ctx.errorsPath, {
|
|
15445
|
+
id: signal.incident.id,
|
|
15446
|
+
timestamp: signal.incident.timestamp,
|
|
15447
|
+
service: signal.incident.service,
|
|
15448
|
+
errorType: signal.incident.errorType,
|
|
15449
|
+
errorMessage: signal.incident.errorMessage,
|
|
15450
|
+
...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
|
|
15451
|
+
affectedNode: resolved.targetNodeId
|
|
15452
|
+
});
|
|
15453
|
+
continue;
|
|
15454
|
+
}
|
|
14983
15455
|
if (resolved.ensureInfraNode) {
|
|
14984
15456
|
const { kind, name, provider } = resolved.ensureInfraNode;
|
|
14985
15457
|
ensureInfraNode(graph, kind, name, provider);
|
|
@@ -17214,6 +17686,338 @@ function createPlanetscaleConnector(graph, config, deps = {}) {
|
|
|
17214
17686
|
};
|
|
17215
17687
|
}
|
|
17216
17688
|
|
|
17689
|
+
// src/connectors/eas/index.ts
|
|
17690
|
+
init_cjs_shims();
|
|
17691
|
+
|
|
17692
|
+
// src/connectors/eas/client.ts
|
|
17693
|
+
init_cjs_shims();
|
|
17694
|
+
|
|
17695
|
+
// src/connectors/eas/types.ts
|
|
17696
|
+
init_cjs_shims();
|
|
17697
|
+
function readEasCredentials(raw) {
|
|
17698
|
+
const token = raw["token"];
|
|
17699
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
17700
|
+
throw new Error("eas connector: credentials.token (EXPO_TOKEN) must be a non-empty string");
|
|
17701
|
+
}
|
|
17702
|
+
return { token };
|
|
17703
|
+
}
|
|
17704
|
+
var EAS_STATUS_ERRORED = "ERRORED";
|
|
17705
|
+
var TRANSIENT_BUILD_PHASES = /* @__PURE__ */ new Set([
|
|
17706
|
+
"SPIN_UP_BUILDER",
|
|
17707
|
+
"PREPARE_CREDENTIALS",
|
|
17708
|
+
"RESTORE_CACHE",
|
|
17709
|
+
"UPLOAD_APPLICATION_ARCHIVE"
|
|
17710
|
+
]);
|
|
17711
|
+
var INTERNAL_ERROR_CODE = /INTERNAL_SERVER_ERROR|EAS_BUILD_.*INTERNAL|_INTERNAL_ERROR|UNKNOWN_ERROR/i;
|
|
17712
|
+
function isTransientFailure(err) {
|
|
17713
|
+
if (!err) return false;
|
|
17714
|
+
const phase = typeof err.buildPhase === "string" ? err.buildPhase : void 0;
|
|
17715
|
+
if (phase) {
|
|
17716
|
+
if (TRANSIENT_BUILD_PHASES.has(phase)) return true;
|
|
17717
|
+
if (/^SPIN_UP|_CREDENTIALS$|^RESTORE_CACHE/i.test(phase)) return true;
|
|
17718
|
+
}
|
|
17719
|
+
const code = typeof err.errorCode === "string" ? err.errorCode : void 0;
|
|
17720
|
+
if (code && INTERNAL_ERROR_CODE.test(code)) return true;
|
|
17721
|
+
return false;
|
|
17722
|
+
}
|
|
17723
|
+
var FIELD_SEP3 = "\0";
|
|
17724
|
+
var EAS_TARGET_KIND = "eas-build";
|
|
17725
|
+
function packEasTargetName(identity) {
|
|
17726
|
+
return [identity.serviceName, identity.phase].join(FIELD_SEP3);
|
|
17727
|
+
}
|
|
17728
|
+
function parseEasTargetName(targetName) {
|
|
17729
|
+
const sep = targetName.indexOf(FIELD_SEP3);
|
|
17730
|
+
if (sep === -1) return null;
|
|
17731
|
+
const serviceName = targetName.slice(0, sep);
|
|
17732
|
+
const phase = targetName.slice(sep + 1);
|
|
17733
|
+
if (!serviceName) return null;
|
|
17734
|
+
return { serviceName, phase };
|
|
17735
|
+
}
|
|
17736
|
+
|
|
17737
|
+
// src/connectors/eas/client.ts
|
|
17738
|
+
var DEFAULT_EAS_API_URL = "https://api.expo.dev/graphql";
|
|
17739
|
+
var DEFAULT_PAGE_SIZE = 50;
|
|
17740
|
+
var DEFAULT_MAX_PAGES = 10;
|
|
17741
|
+
var DEFAULT_MAX_LOG_BYTES = 16 * 1024;
|
|
17742
|
+
var DEFAULT_MAX_LOOKBACK_MS6 = 7 * 24 * 60 * 60 * 1e3;
|
|
17743
|
+
var BUILDS_QUERY = `
|
|
17744
|
+
query NeatEasErroredBuilds($appId: String!, $offset: Int!, $limit: Int!) {
|
|
17745
|
+
app {
|
|
17746
|
+
byId(appId: $appId) {
|
|
17747
|
+
builds(offset: $offset, limit: $limit, filter: { status: ERRORED }) {
|
|
17748
|
+
id
|
|
17749
|
+
status
|
|
17750
|
+
platform
|
|
17751
|
+
buildProfile
|
|
17752
|
+
gitCommitHash
|
|
17753
|
+
gitCommitMessage
|
|
17754
|
+
gitRef
|
|
17755
|
+
isGitWorkingTreeDirty
|
|
17756
|
+
createdAt
|
|
17757
|
+
completedAt
|
|
17758
|
+
error {
|
|
17759
|
+
buildPhase
|
|
17760
|
+
errorCode
|
|
17761
|
+
message
|
|
17762
|
+
docsUrl
|
|
17763
|
+
}
|
|
17764
|
+
logFileUrls
|
|
17765
|
+
}
|
|
17766
|
+
}
|
|
17767
|
+
}
|
|
17768
|
+
}
|
|
17769
|
+
`;
|
|
17770
|
+
async function easGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
|
|
17771
|
+
const res = await junctionFetch(
|
|
17772
|
+
apiUrl,
|
|
17773
|
+
{
|
|
17774
|
+
method: "POST",
|
|
17775
|
+
headers: {
|
|
17776
|
+
"Content-Type": "application/json",
|
|
17777
|
+
...bearerAuthHeader(token)
|
|
17778
|
+
},
|
|
17779
|
+
body: JSON.stringify({ query, variables })
|
|
17780
|
+
},
|
|
17781
|
+
// accountKey: the Expo app id — the per-(provider, accountKey) rate-limit
|
|
17782
|
+
// bucket (ADR-131), the closest thing this connector carries to "one account".
|
|
17783
|
+
{ provider: "eas", accountKey, ...fetchImpl ? { fetchImpl } : {} }
|
|
17784
|
+
);
|
|
17785
|
+
if (!res.ok) {
|
|
17786
|
+
throw new Error(`Expo GraphQL request failed: ${res.status} ${res.statusText}`);
|
|
17787
|
+
}
|
|
17788
|
+
const body = await res.json();
|
|
17789
|
+
if (body.errors && body.errors.length > 0) {
|
|
17790
|
+
throw new Error(`Expo GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
|
|
17791
|
+
}
|
|
17792
|
+
if (!body.data) throw new Error("Expo GraphQL response carried no data");
|
|
17793
|
+
return body.data;
|
|
17794
|
+
}
|
|
17795
|
+
async function fetchErroredBuilds(token, config, fetchImpl) {
|
|
17796
|
+
const apiUrl = config.apiUrl ?? DEFAULT_EAS_API_URL;
|
|
17797
|
+
const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
|
|
17798
|
+
const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
|
|
17799
|
+
const out = [];
|
|
17800
|
+
const seen = /* @__PURE__ */ new Set();
|
|
17801
|
+
for (let page = 0; page < maxPages; page++) {
|
|
17802
|
+
const data = await easGraphQL(
|
|
17803
|
+
apiUrl,
|
|
17804
|
+
token,
|
|
17805
|
+
BUILDS_QUERY,
|
|
17806
|
+
{ appId: config.appId, offset: page * pageSize, limit: pageSize },
|
|
17807
|
+
config.appId,
|
|
17808
|
+
fetchImpl
|
|
17809
|
+
);
|
|
17810
|
+
const builds = data.app?.byId?.builds;
|
|
17811
|
+
if (!Array.isArray(builds)) break;
|
|
17812
|
+
let added = 0;
|
|
17813
|
+
for (const b of builds) {
|
|
17814
|
+
if (!b || typeof b.id !== "string" || b.id.length === 0) continue;
|
|
17815
|
+
if (b.status !== EAS_STATUS_ERRORED) continue;
|
|
17816
|
+
if (seen.has(b.id)) continue;
|
|
17817
|
+
seen.add(b.id);
|
|
17818
|
+
out.push(b);
|
|
17819
|
+
added++;
|
|
17820
|
+
}
|
|
17821
|
+
if (builds.length < pageSize) break;
|
|
17822
|
+
if (added === 0) break;
|
|
17823
|
+
}
|
|
17824
|
+
return out;
|
|
17825
|
+
}
|
|
17826
|
+
async function fetchBuildLogs(logFileUrls, maxBytes = DEFAULT_MAX_LOG_BYTES, fetchImpl) {
|
|
17827
|
+
if (!Array.isArray(logFileUrls) || logFileUrls.length === 0) return "";
|
|
17828
|
+
const doFetch = fetchImpl ?? fetch;
|
|
17829
|
+
const chunks = [];
|
|
17830
|
+
for (const url of logFileUrls) {
|
|
17831
|
+
if (typeof url !== "string" || url.length === 0) continue;
|
|
17832
|
+
try {
|
|
17833
|
+
const res = await doFetch(url);
|
|
17834
|
+
if (!res.ok) continue;
|
|
17835
|
+
chunks.push(await res.text());
|
|
17836
|
+
} catch {
|
|
17837
|
+
}
|
|
17838
|
+
}
|
|
17839
|
+
const joined = chunks.join("\n");
|
|
17840
|
+
return joined.length > maxBytes ? joined.slice(joined.length - maxBytes) : joined;
|
|
17841
|
+
}
|
|
17842
|
+
|
|
17843
|
+
// src/connectors/eas/map.ts
|
|
17844
|
+
init_cjs_shims();
|
|
17845
|
+
function buildEventTime(build) {
|
|
17846
|
+
if (typeof build.completedAt === "string" && build.completedAt.length > 0) return build.completedAt;
|
|
17847
|
+
if (typeof build.createdAt === "string" && build.createdAt.length > 0) return build.createdAt;
|
|
17848
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
17849
|
+
}
|
|
17850
|
+
function incidentMessage2(build) {
|
|
17851
|
+
const err = build.error ?? {};
|
|
17852
|
+
const phase = typeof err.buildPhase === "string" && err.buildPhase.length > 0 ? ` at ${err.buildPhase}` : "";
|
|
17853
|
+
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";
|
|
17854
|
+
let msg = `EAS build failed${phase}: ${detail}`;
|
|
17855
|
+
if (build.isGitWorkingTreeDirty === true) {
|
|
17856
|
+
msg += " (built from a dirty working tree \u2014 the commit may not represent what built)";
|
|
17857
|
+
}
|
|
17858
|
+
return msg;
|
|
17859
|
+
}
|
|
17860
|
+
function incidentAttributes(build) {
|
|
17861
|
+
const attrs = {};
|
|
17862
|
+
const err = build.error ?? {};
|
|
17863
|
+
const put = (k, v) => {
|
|
17864
|
+
if (typeof v === "string" && v.length === 0) return;
|
|
17865
|
+
if (v !== void 0 && v !== null) attrs[k] = v;
|
|
17866
|
+
};
|
|
17867
|
+
put("eas.buildId", build.id);
|
|
17868
|
+
put("eas.platform", build.platform ?? void 0);
|
|
17869
|
+
put("eas.buildProfile", build.buildProfile ?? void 0);
|
|
17870
|
+
put("eas.buildPhase", err.buildPhase ?? void 0);
|
|
17871
|
+
put("eas.errorCode", err.errorCode ?? void 0);
|
|
17872
|
+
put("eas.docsUrl", err.docsUrl ?? void 0);
|
|
17873
|
+
put("eas.gitCommitHash", build.gitCommitHash ?? void 0);
|
|
17874
|
+
put("eas.gitRef", build.gitRef ?? void 0);
|
|
17875
|
+
put("eas.gitCommitMessage", build.gitCommitMessage ?? void 0);
|
|
17876
|
+
if (typeof build.isGitWorkingTreeDirty === "boolean") {
|
|
17877
|
+
attrs["eas.gitWorkingTreeDirty"] = build.isGitWorkingTreeDirty;
|
|
17878
|
+
if (build.isGitWorkingTreeDirty) attrs["eas.confidence"] = "low";
|
|
17879
|
+
}
|
|
17880
|
+
put("eas.createdAt", build.createdAt ?? void 0);
|
|
17881
|
+
put("eas.completedAt", build.completedAt ?? void 0);
|
|
17882
|
+
if (typeof build.logsText === "string" && build.logsText.length > 0) {
|
|
17883
|
+
attrs["eas.logs"] = build.logsText;
|
|
17884
|
+
}
|
|
17885
|
+
return attrs;
|
|
17886
|
+
}
|
|
17887
|
+
function mapBuildToSignal(build, serviceName) {
|
|
17888
|
+
if (!build || typeof build !== "object") return null;
|
|
17889
|
+
if (build.status !== EAS_STATUS_ERRORED) return null;
|
|
17890
|
+
if (!build.error) return null;
|
|
17891
|
+
if (isTransientFailure(build.error)) return null;
|
|
17892
|
+
const timestamp = buildEventTime(build);
|
|
17893
|
+
const phase = typeof build.error.buildPhase === "string" ? build.error.buildPhase : "";
|
|
17894
|
+
return {
|
|
17895
|
+
targetKind: EAS_TARGET_KIND,
|
|
17896
|
+
targetName: packEasTargetName({ serviceName, phase }),
|
|
17897
|
+
// Incident-only — no edge, so no call/error count to replay.
|
|
17898
|
+
callCount: 0,
|
|
17899
|
+
errorCount: 0,
|
|
17900
|
+
lastObservedIso: timestamp,
|
|
17901
|
+
incident: {
|
|
17902
|
+
id: `eas:build:${build.id}`,
|
|
17903
|
+
timestamp,
|
|
17904
|
+
service: serviceName,
|
|
17905
|
+
errorType: "eas-build-failure",
|
|
17906
|
+
errorMessage: incidentMessage2(build),
|
|
17907
|
+
attributes: incidentAttributes(build)
|
|
17908
|
+
}
|
|
17909
|
+
};
|
|
17910
|
+
}
|
|
17911
|
+
function mapBuildsToSignals(builds, serviceName) {
|
|
17912
|
+
const out = [];
|
|
17913
|
+
for (const build of builds) {
|
|
17914
|
+
const signal = mapBuildToSignal(build, serviceName);
|
|
17915
|
+
if (signal) out.push(signal);
|
|
17916
|
+
}
|
|
17917
|
+
return out;
|
|
17918
|
+
}
|
|
17919
|
+
|
|
17920
|
+
// src/connectors/eas/resolve.ts
|
|
17921
|
+
init_cjs_shims();
|
|
17922
|
+
var import_types82 = require("@neat.is/types");
|
|
17923
|
+
var NO_ENV2 = "unknown";
|
|
17924
|
+
var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
|
|
17925
|
+
var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
|
|
17926
|
+
"READ_APP_CONFIG",
|
|
17927
|
+
"CONFIGURE_EXPO_UPDATES",
|
|
17928
|
+
"CALCULATE_EXPO_UPDATES_RUNTIME_VERSION"
|
|
17929
|
+
]);
|
|
17930
|
+
function configBasenamesForPhase(phase) {
|
|
17931
|
+
if (EAS_JSON_PHASES.has(phase)) return ["eas.json"];
|
|
17932
|
+
if (APP_CONFIG_PHASES.has(phase)) return ["app.json", "app.config.json"];
|
|
17933
|
+
return [];
|
|
17934
|
+
}
|
|
17935
|
+
function configNodeService(graph, configNodeId) {
|
|
17936
|
+
for (const edgeId of graph.inboundEdges(configNodeId)) {
|
|
17937
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
17938
|
+
if (edge.type !== import_types82.EdgeType.CONFIGURED_BY) continue;
|
|
17939
|
+
const parsed = (0, import_types82.parseFileId)(edge.source);
|
|
17940
|
+
if (parsed) return parsed.service;
|
|
17941
|
+
}
|
|
17942
|
+
return null;
|
|
17943
|
+
}
|
|
17944
|
+
function findConfigNode(graph, basenames, serviceName) {
|
|
17945
|
+
let scoped = null;
|
|
17946
|
+
let anyMatch = null;
|
|
17947
|
+
graph.forEachNode((id, attrs) => {
|
|
17948
|
+
if (scoped) return;
|
|
17949
|
+
const node = attrs;
|
|
17950
|
+
if (node.type !== import_types82.NodeType.ConfigNode) return;
|
|
17951
|
+
if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
|
|
17952
|
+
if (anyMatch === null) anyMatch = id;
|
|
17953
|
+
if (configNodeService(graph, id) === serviceName) scoped = id;
|
|
17954
|
+
});
|
|
17955
|
+
return scoped ?? anyMatch;
|
|
17956
|
+
}
|
|
17957
|
+
function createEasResolveTarget(graph) {
|
|
17958
|
+
return (signal) => {
|
|
17959
|
+
if (signal.targetKind !== EAS_TARGET_KIND) return null;
|
|
17960
|
+
const identity = parseEasTargetName(signal.targetName);
|
|
17961
|
+
if (!identity) return null;
|
|
17962
|
+
const { serviceName, phase } = identity;
|
|
17963
|
+
const basenames = configBasenamesForPhase(phase);
|
|
17964
|
+
if (basenames.length > 0) {
|
|
17965
|
+
const configNodeId = findConfigNode(graph, basenames, serviceName);
|
|
17966
|
+
if (configNodeId) {
|
|
17967
|
+
return { targetNodeId: configNodeId, serviceName, edgeType: import_types82.EdgeType.CALLS };
|
|
17968
|
+
}
|
|
17969
|
+
}
|
|
17970
|
+
return {
|
|
17971
|
+
targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
|
|
17972
|
+
serviceName,
|
|
17973
|
+
edgeType: import_types82.EdgeType.CALLS
|
|
17974
|
+
};
|
|
17975
|
+
};
|
|
17976
|
+
}
|
|
17977
|
+
|
|
17978
|
+
// src/connectors/eas/index.ts
|
|
17979
|
+
function isBuildSince(build, sinceIso) {
|
|
17980
|
+
const t = Date.parse(buildEventTime(build));
|
|
17981
|
+
const s = Date.parse(sinceIso);
|
|
17982
|
+
if (Number.isNaN(t) || Number.isNaN(s)) return true;
|
|
17983
|
+
return t > s;
|
|
17984
|
+
}
|
|
17985
|
+
function boundedSinceIso2(since, now, maxLookbackMs) {
|
|
17986
|
+
const floor = new Date(now.getTime() - maxLookbackMs);
|
|
17987
|
+
if (!since) return floor.toISOString();
|
|
17988
|
+
const sinceMs = new Date(since).getTime();
|
|
17989
|
+
if (Number.isNaN(sinceMs)) return floor.toISOString();
|
|
17990
|
+
return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
|
|
17991
|
+
}
|
|
17992
|
+
var EasConnector = class {
|
|
17993
|
+
constructor(config, fetchImpl) {
|
|
17994
|
+
this.config = config;
|
|
17995
|
+
this.fetchImpl = fetchImpl;
|
|
17996
|
+
}
|
|
17997
|
+
config;
|
|
17998
|
+
fetchImpl;
|
|
17999
|
+
provider = "eas";
|
|
18000
|
+
async poll(ctx) {
|
|
18001
|
+
const creds = readEasCredentials(ctx.credentials);
|
|
18002
|
+
const serviceName = this.config.serviceName ?? this.config.appId;
|
|
18003
|
+
const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS6;
|
|
18004
|
+
const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
|
|
18005
|
+
const builds = await fetchErroredBuilds(creds.token, this.config, this.fetchImpl);
|
|
18006
|
+
const fresh = builds.filter((b) => isBuildSince(b, sinceIso));
|
|
18007
|
+
const maxLogBytes = this.config.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
|
|
18008
|
+
for (const build of fresh) {
|
|
18009
|
+
build.logsText = await fetchBuildLogs(build.logFileUrls, maxLogBytes, this.fetchImpl);
|
|
18010
|
+
}
|
|
18011
|
+
return mapBuildsToSignals(fresh, serviceName);
|
|
18012
|
+
}
|
|
18013
|
+
};
|
|
18014
|
+
function createEasConnector(graph, config, fetchImpl) {
|
|
18015
|
+
return {
|
|
18016
|
+
connector: new EasConnector(config, fetchImpl),
|
|
18017
|
+
resolveTarget: createEasResolveTarget(graph)
|
|
18018
|
+
};
|
|
18019
|
+
}
|
|
18020
|
+
|
|
17217
18021
|
// src/connectors/registry.ts
|
|
17218
18022
|
var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
|
|
17219
18023
|
async function authProbe(input) {
|
|
@@ -17501,6 +18305,41 @@ var PROVIDER_DISPATCH = {
|
|
|
17501
18305
|
...fetchImpl ? { fetchImpl } : {}
|
|
17502
18306
|
});
|
|
17503
18307
|
}
|
|
18308
|
+
},
|
|
18309
|
+
eas: {
|
|
18310
|
+
provider: "eas",
|
|
18311
|
+
// The secret is a single robot-user EXPO_TOKEN; a single-string credential
|
|
18312
|
+
// maps to `token`. `appId` is non-secret config (connector-config.md §7.1).
|
|
18313
|
+
primaryCredentialKey: "token",
|
|
18314
|
+
requiredCredentialFields: ["token"],
|
|
18315
|
+
requiredOptionFields: ["appId"],
|
|
18316
|
+
build(graph, options) {
|
|
18317
|
+
return createEasConnector(graph, options);
|
|
18318
|
+
},
|
|
18319
|
+
// Runs the connector's real `builds` query at limit 1 — the exact read poll()
|
|
18320
|
+
// performs, minus the pages — so the probe checks both that the EXPO_TOKEN
|
|
18321
|
+
// authenticates and that this app id is reachable, the same probe-the-real-
|
|
18322
|
+
// query discipline Railway and Cloud Run use over a trivial `{ __typename }`.
|
|
18323
|
+
// A bad or wrong-scoped token comes back as an Expo GraphQL error, which
|
|
18324
|
+
// `fetchErroredBuilds` throws on, so it fails honestly here rather than
|
|
18325
|
+
// silently at the first poll.
|
|
18326
|
+
async validate({ credentials, options, fetchImpl }) {
|
|
18327
|
+
const cfg = options;
|
|
18328
|
+
const appId = String(cfg.appId ?? "");
|
|
18329
|
+
if (!appId) return { ok: false, reason: "eas: appId is required to validate" };
|
|
18330
|
+
const probeConfig = {
|
|
18331
|
+
appId,
|
|
18332
|
+
pageSize: 1,
|
|
18333
|
+
maxPages: 1,
|
|
18334
|
+
...cfg.apiUrl ? { apiUrl: cfg.apiUrl } : {}
|
|
18335
|
+
};
|
|
18336
|
+
try {
|
|
18337
|
+
await fetchErroredBuilds(String(credentials.token ?? ""), probeConfig, fetchImpl);
|
|
18338
|
+
return { ok: true };
|
|
18339
|
+
} catch (err) {
|
|
18340
|
+
return { ok: false, reason: `eas auth check failed: ${err.message}` };
|
|
18341
|
+
}
|
|
18342
|
+
}
|
|
17504
18343
|
}
|
|
17505
18344
|
};
|
|
17506
18345
|
function vercelCredsFrom(credentials) {
|
|
@@ -17682,7 +18521,11 @@ async function startConnectorPolling(input) {
|
|
|
17682
18521
|
const stopFns = all.map(
|
|
17683
18522
|
(registration) => startConnectorPollLoop(
|
|
17684
18523
|
registration.connector,
|
|
17685
|
-
{
|
|
18524
|
+
{
|
|
18525
|
+
projectDir: input.projectDir,
|
|
18526
|
+
credentials: registration.credentials,
|
|
18527
|
+
...input.errorsPath ? { errorsPath: input.errorsPath } : {}
|
|
18528
|
+
},
|
|
17686
18529
|
input.graph,
|
|
17687
18530
|
registration.resolveTarget,
|
|
17688
18531
|
{ intervalMs: registration.intervalMs, connectorId: registration.id }
|
|
@@ -17852,11 +18695,11 @@ function registerRoutes(scope, ctx) {
|
|
|
17852
18695
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
17853
18696
|
const parsed = [];
|
|
17854
18697
|
for (const c of candidates) {
|
|
17855
|
-
const r =
|
|
18698
|
+
const r = import_types85.DivergenceTypeSchema.safeParse(c);
|
|
17856
18699
|
if (!r.success) {
|
|
17857
18700
|
return reply.code(400).send({
|
|
17858
18701
|
error: `unknown divergence type "${c}"`,
|
|
17859
|
-
allowed:
|
|
18702
|
+
allowed: import_types85.DivergenceTypeSchema.options
|
|
17860
18703
|
});
|
|
17861
18704
|
}
|
|
17862
18705
|
parsed.push(r.data);
|
|
@@ -17963,10 +18806,15 @@ function registerRoutes(scope, ctx) {
|
|
|
17963
18806
|
}
|
|
17964
18807
|
const reg = built.registration;
|
|
17965
18808
|
const at = (/* @__PURE__ */ new Date()).toISOString();
|
|
18809
|
+
const incidentsPath = errorsPathFor(proj);
|
|
17966
18810
|
try {
|
|
17967
18811
|
const result = await ctx.runPoll(
|
|
17968
18812
|
reg.connector,
|
|
17969
|
-
{
|
|
18813
|
+
{
|
|
18814
|
+
projectDir: proj.scanPath ?? "",
|
|
18815
|
+
credentials: reg.credentials,
|
|
18816
|
+
...incidentsPath ? { errorsPath: incidentsPath } : {}
|
|
18817
|
+
},
|
|
17970
18818
|
proj.graph,
|
|
17971
18819
|
reg.resolveTarget
|
|
17972
18820
|
);
|
|
@@ -18036,6 +18884,34 @@ function registerRoutes(scope, ctx) {
|
|
|
18036
18884
|
}
|
|
18037
18885
|
return getBlastRadius(proj.graph, nodeId, depth);
|
|
18038
18886
|
});
|
|
18887
|
+
scope.get("/graph/expand/:nodeId", async (req, reply) => {
|
|
18888
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18889
|
+
if (!proj) return;
|
|
18890
|
+
const { nodeId } = req.params;
|
|
18891
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
18892
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
18893
|
+
}
|
|
18894
|
+
const direction = req.query.direction;
|
|
18895
|
+
if (direction !== "up" && direction !== "down") {
|
|
18896
|
+
return reply.code(400).send({ error: 'direction must be "up" or "down"' });
|
|
18897
|
+
}
|
|
18898
|
+
const epath = errorsPathFor(proj);
|
|
18899
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
18900
|
+
return expandNode(proj.graph, nodeId, direction, incidents);
|
|
18901
|
+
});
|
|
18902
|
+
scope.get("/graph/relate", async (req, reply) => {
|
|
18903
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18904
|
+
if (!proj) return;
|
|
18905
|
+
const { a, b } = req.query;
|
|
18906
|
+
if (!a || !b) {
|
|
18907
|
+
return reply.code(400).send({ error: "both a and b query params are required" });
|
|
18908
|
+
}
|
|
18909
|
+
const maxDepth = req.query.maxDepth ? Number(req.query.maxDepth) : void 0;
|
|
18910
|
+
if (maxDepth !== void 0 && (!Number.isFinite(maxDepth) || maxDepth < 1)) {
|
|
18911
|
+
return reply.code(400).send({ error: "maxDepth must be a positive integer" });
|
|
18912
|
+
}
|
|
18913
|
+
return relate(proj.graph, a, b, maxDepth);
|
|
18914
|
+
});
|
|
18039
18915
|
scope.get("/search", async (req, reply) => {
|
|
18040
18916
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18041
18917
|
if (!proj) return;
|
|
@@ -18165,7 +19041,7 @@ function registerRoutes(scope, ctx) {
|
|
|
18165
19041
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
18166
19042
|
let violations = await log.readAll();
|
|
18167
19043
|
if (req.query.severity) {
|
|
18168
|
-
const sev =
|
|
19044
|
+
const sev = import_types85.PolicySeveritySchema.safeParse(req.query.severity);
|
|
18169
19045
|
if (!sev.success) {
|
|
18170
19046
|
return reply.code(400).send({
|
|
18171
19047
|
error: "invalid severity",
|
|
@@ -18204,7 +19080,7 @@ function registerRoutes(scope, ctx) {
|
|
|
18204
19080
|
scope.post("/policies/check", async (req, reply) => {
|
|
18205
19081
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
18206
19082
|
if (!proj) return;
|
|
18207
|
-
const parsed =
|
|
19083
|
+
const parsed = import_types85.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
18208
19084
|
if (!parsed.success) {
|
|
18209
19085
|
return reply.code(400).send({
|
|
18210
19086
|
error: "invalid /policies/check body",
|
|
@@ -18553,7 +19429,7 @@ function unroutedErrorsPath(neatHome3) {
|
|
|
18553
19429
|
}
|
|
18554
19430
|
|
|
18555
19431
|
// src/daemon.ts
|
|
18556
|
-
var
|
|
19432
|
+
var import_types86 = require("@neat.is/types");
|
|
18557
19433
|
function daemonJsonPath(scanPath) {
|
|
18558
19434
|
return import_node_path67.default.join(scanPath, "neat-out", "daemon.json");
|
|
18559
19435
|
}
|
|
@@ -18678,7 +19554,7 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
|
|
|
18678
19554
|
if (!serviceName) return true;
|
|
18679
19555
|
if (serviceNameMatchesProject(serviceName, project)) return true;
|
|
18680
19556
|
return graph.someNode(
|
|
18681
|
-
(_id, attrs) => attrs.type ===
|
|
19557
|
+
(_id, attrs) => attrs.type === import_types86.NodeType.ServiceNode && attrs.name === serviceName
|
|
18682
19558
|
);
|
|
18683
19559
|
}
|
|
18684
19560
|
async function bootstrapProject(entry, connectors = [], neatHome3) {
|
|
@@ -18726,6 +19602,10 @@ async function bootstrapProject(entry, connectors = [], neatHome3) {
|
|
|
18726
19602
|
project: entry.name,
|
|
18727
19603
|
graph,
|
|
18728
19604
|
projectDir: entry.path,
|
|
19605
|
+
// The slot's incident ledger, so an incident-emitting connector (ADR-185)
|
|
19606
|
+
// writes a build-failure incident onto the same errors.ndjson OTLP-derived
|
|
19607
|
+
// incidents land in.
|
|
19608
|
+
errorsPath: paths.errorsPath,
|
|
18729
19609
|
...neatHome3 ? { home: neatHome3 } : {},
|
|
18730
19610
|
extra: connectors,
|
|
18731
19611
|
onSkip: (skipped, reason) => console.warn(
|