@neat.is/core 0.9.2 → 0.9.3
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-TMHCS4ZY.js → chunk-2D4Y5QHE.js} +2 -2
- package/dist/{chunk-HD5X5TWY.js → chunk-EDE4XP2M.js} +287 -32
- package/dist/chunk-EDE4XP2M.js.map +1 -0
- package/dist/cli.cjs +297 -32
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +15 -3
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +285 -31
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +285 -31
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +285 -31
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-HD5X5TWY.js.map +0 -1
- /package/dist/{chunk-TMHCS4ZY.js.map → chunk-2D4Y5QHE.js.map} +0 -0
package/dist/index.d.cts
CHANGED
|
@@ -441,7 +441,9 @@ interface StalenessLoopOptions {
|
|
|
441
441
|
onPolicyTrigger?: (graph: NeatGraph) => Promise<void> | void;
|
|
442
442
|
}
|
|
443
443
|
declare function startStalenessLoop(graph: NeatGraph, options?: StalenessLoopOptions): () => void;
|
|
444
|
-
declare function readErrorEvents(errorsPath: string
|
|
444
|
+
declare function readErrorEvents(errorsPath: string, opts?: {
|
|
445
|
+
limit?: number;
|
|
446
|
+
}): Promise<ErrorEvent[]>;
|
|
445
447
|
|
|
446
448
|
declare function confidenceForEdge(edge: GraphEdge, now?: number): number;
|
|
447
449
|
declare function getBlastRadius(graph: NeatGraph, nodeId: string, maxDepth?: number): BlastRadiusResult;
|
package/dist/index.d.ts
CHANGED
|
@@ -441,7 +441,9 @@ interface StalenessLoopOptions {
|
|
|
441
441
|
onPolicyTrigger?: (graph: NeatGraph) => Promise<void> | void;
|
|
442
442
|
}
|
|
443
443
|
declare function startStalenessLoop(graph: NeatGraph, options?: StalenessLoopOptions): () => void;
|
|
444
|
-
declare function readErrorEvents(errorsPath: string
|
|
444
|
+
declare function readErrorEvents(errorsPath: string, opts?: {
|
|
445
|
+
limit?: number;
|
|
446
|
+
}): Promise<ErrorEvent[]>;
|
|
445
447
|
|
|
446
448
|
declare function confidenceForEdge(edge: GraphEdge, now?: number): number;
|
|
447
449
|
declare function getBlastRadius(graph: NeatGraph, nodeId: string, maxDepth?: number): BlastRadiusResult;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
routeSpanToProject,
|
|
3
3
|
startDaemon
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2D4Y5QHE.js";
|
|
5
5
|
import {
|
|
6
6
|
ProjectNameCollisionError,
|
|
7
7
|
addProject,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
thresholdForEdgeType,
|
|
38
38
|
touchLastSeen,
|
|
39
39
|
writeAtomically
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-EDE4XP2M.js";
|
|
41
41
|
import {
|
|
42
42
|
startOtelGrpcReceiver
|
|
43
43
|
} from "./chunk-ERE47MCR.js";
|
package/dist/neatd.cjs
CHANGED
|
@@ -1888,6 +1888,47 @@ function classifyNode(ctx) {
|
|
|
1888
1888
|
function isVictimSeed(ctx) {
|
|
1889
1889
|
return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
|
|
1890
1890
|
}
|
|
1891
|
+
var OUTBOUND_CONNECTION_FAILURE_PATTERNS = [
|
|
1892
|
+
"name resolution",
|
|
1893
|
+
"resolve host",
|
|
1894
|
+
"getaddrinfo",
|
|
1895
|
+
"enotfound",
|
|
1896
|
+
"connection refused",
|
|
1897
|
+
"econnrefused",
|
|
1898
|
+
"connection reset",
|
|
1899
|
+
"econnreset",
|
|
1900
|
+
"able to connect",
|
|
1901
|
+
"failed to connect",
|
|
1902
|
+
"cannot connect",
|
|
1903
|
+
"could not connect",
|
|
1904
|
+
"unable to connect",
|
|
1905
|
+
"connection timed out",
|
|
1906
|
+
"etimedout",
|
|
1907
|
+
"no route to host",
|
|
1908
|
+
"host unreachable",
|
|
1909
|
+
"network is unreachable",
|
|
1910
|
+
"connection closed"
|
|
1911
|
+
];
|
|
1912
|
+
function incidentTextIndicatesOutboundFailure(ev) {
|
|
1913
|
+
const haystack = [ev.errorMessage, ev.errorType, ev.exceptionType, ev.exceptionStacktrace].filter((s) => typeof s === "string").join(" ").toLowerCase();
|
|
1914
|
+
return OUTBOUND_CONNECTION_FAILURE_PATTERNS.some((p) => haystack.includes(p));
|
|
1915
|
+
}
|
|
1916
|
+
function hasFailingOutbound(graph, nodeId, seedSource, incidents) {
|
|
1917
|
+
for (const n of nodeScope(graph, nodeId)) {
|
|
1918
|
+
if (!graph.hasNode(n)) continue;
|
|
1919
|
+
for (const edgeId of graph.outboundEdges(n)) {
|
|
1920
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1921
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1922
|
+
if ((e.signal?.errorCount ?? 0) > 0) return true;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (seedSource === "incident" && incidents) {
|
|
1926
|
+
for (const ev of incidents) {
|
|
1927
|
+
if (incidentMatchesNode(ev, nodeId) && incidentTextIndicatesOutboundFailure(ev)) return true;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
return false;
|
|
1931
|
+
}
|
|
1891
1932
|
function grainOf(graph, nodeId) {
|
|
1892
1933
|
if (!graph.hasNode(nodeId)) return "unknown";
|
|
1893
1934
|
const t = graph.getNodeAttributes(nodeId).type;
|
|
@@ -2051,7 +2092,8 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
|
|
|
2051
2092
|
const candidates = [];
|
|
2052
2093
|
const deadEndOnSymptom = tagged.source === "incident" && seedNode === errorNodeId && legacy.traversalPath.length === 1;
|
|
2053
2094
|
const staleChain = deadEndOnSymptom && !(seedCtx && isVictimSeed(seedCtx)) ? followStaleCallChain(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH) : null;
|
|
2054
|
-
|
|
2095
|
+
const seedFailsOutbound = seedCtx !== null && hasFailingOutbound(graph, seedNode, tagged.source, incidents);
|
|
2096
|
+
if (seedCtx && isVictimSeed(seedCtx) && !seedFailsOutbound) {
|
|
2055
2097
|
const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
|
|
2056
2098
|
const staleNote = seedCtx.stale ? "; it has gone STALE under load" : "";
|
|
2057
2099
|
const satNote = isSaturated(seedCtx) ? `; its inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
|
|
@@ -6808,15 +6850,36 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
6808
6850
|
clearInterval(interval);
|
|
6809
6851
|
};
|
|
6810
6852
|
}
|
|
6811
|
-
|
|
6853
|
+
var INCIDENT_READ_MAX_BYTES = 32 * 1024 * 1024;
|
|
6854
|
+
var INCIDENT_READ_MAX_EVENTS = 5e3;
|
|
6855
|
+
async function readErrorFileTail(errorsPath, maxBytes) {
|
|
6856
|
+
const handle = await import_node_fs7.promises.open(errorsPath, "r");
|
|
6812
6857
|
try {
|
|
6813
|
-
const
|
|
6814
|
-
|
|
6815
|
-
|
|
6858
|
+
const { size } = await handle.stat();
|
|
6859
|
+
if (size <= maxBytes) {
|
|
6860
|
+
return (await handle.readFile()).toString("utf8");
|
|
6861
|
+
}
|
|
6862
|
+
const buf = Buffer.alloc(maxBytes);
|
|
6863
|
+
await handle.read(buf, 0, maxBytes, size - maxBytes);
|
|
6864
|
+
const raw = buf.toString("utf8");
|
|
6865
|
+
const firstNewline = raw.indexOf("\n");
|
|
6866
|
+
return firstNewline === -1 ? "" : raw.slice(firstNewline + 1);
|
|
6867
|
+
} finally {
|
|
6868
|
+
await handle.close();
|
|
6869
|
+
}
|
|
6870
|
+
}
|
|
6871
|
+
async function readErrorEvents(errorsPath, opts) {
|
|
6872
|
+
let raw;
|
|
6873
|
+
try {
|
|
6874
|
+
raw = await readErrorFileTail(errorsPath, INCIDENT_READ_MAX_BYTES);
|
|
6816
6875
|
} catch (err) {
|
|
6817
6876
|
if (err.code === "ENOENT") return [];
|
|
6818
6877
|
throw err;
|
|
6819
6878
|
}
|
|
6879
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
6880
|
+
const deduped = dedupeIncidents(events);
|
|
6881
|
+
const cap = opts?.limit !== void 0 && opts.limit > 0 ? Math.min(opts.limit, INCIDENT_READ_MAX_EVENTS) : INCIDENT_READ_MAX_EVENTS;
|
|
6882
|
+
return deduped.length > cap ? deduped.slice(deduped.length - cap) : deduped;
|
|
6820
6883
|
}
|
|
6821
6884
|
function isSynthesizedHttpIncident(ev) {
|
|
6822
6885
|
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
@@ -9852,12 +9915,12 @@ async function resolveConfigs(literals, keys, serviceDir, looksLike, parse11) {
|
|
|
9852
9915
|
const value = await interpolateEnvRefs(raw, serviceDir);
|
|
9853
9916
|
if (!looksLike(value)) continue;
|
|
9854
9917
|
const parsed = parse11(value);
|
|
9855
|
-
if (parsed) out.push(parsed);
|
|
9918
|
+
if (parsed) out.push({ ...parsed, hostSource: "config" });
|
|
9856
9919
|
}
|
|
9857
9920
|
for (const lit of literals) {
|
|
9858
9921
|
if (!looksLike(lit)) continue;
|
|
9859
9922
|
const parsed = parse11(lit);
|
|
9860
|
-
if (parsed) out.push(parsed);
|
|
9923
|
+
if (parsed) out.push({ ...parsed, hostSource: "literal" });
|
|
9861
9924
|
}
|
|
9862
9925
|
return out;
|
|
9863
9926
|
}
|
|
@@ -10126,7 +10189,10 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
10126
10189
|
type: import_types23.EdgeType.CONNECTS_TO,
|
|
10127
10190
|
provenance: import_types23.Provenance.EXTRACTED,
|
|
10128
10191
|
confidence: (0, import_types23.confidenceForExtracted)("structural"),
|
|
10129
|
-
|
|
10192
|
+
// Carry how the host was recovered (ADR-213) so the divergence ranker
|
|
10193
|
+
// can tell a real declared store from a hardcoded fault-injection /
|
|
10194
|
+
// flag-gated probe. Only set when the parser distinguished the two.
|
|
10195
|
+
evidence: config.hostSource ? { file: evidenceFile, hostSource: config.hostSource } : { file: evidenceFile }
|
|
10130
10196
|
};
|
|
10131
10197
|
if (!graph.hasEdge(edge.id)) {
|
|
10132
10198
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -16452,9 +16518,170 @@ function detectColumnDrift(node) {
|
|
|
16452
16518
|
}
|
|
16453
16519
|
return out;
|
|
16454
16520
|
}
|
|
16521
|
+
var SYMBOL_MISMATCH_PATTERNS = [
|
|
16522
|
+
{
|
|
16523
|
+
// "'ListProductsResponse' object has no attribute 'products_list'" and kin.
|
|
16524
|
+
kind: "missing-attribute",
|
|
16525
|
+
patterns: [/\bhas no attribute\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i]
|
|
16526
|
+
},
|
|
16527
|
+
{
|
|
16528
|
+
// "object has no field 'X'", "no such field X", "unknown field X".
|
|
16529
|
+
kind: "missing-field",
|
|
16530
|
+
patterns: [
|
|
16531
|
+
/\b(?:has no field|no such field|unknown field)\b(?:\s+named)?[\s:=]*['"`]?([A-Za-z_$][\w$.]*)['"`]?/i
|
|
16532
|
+
]
|
|
16533
|
+
},
|
|
16534
|
+
{
|
|
16535
|
+
// "has no property X", "no property named X".
|
|
16536
|
+
kind: "missing-property",
|
|
16537
|
+
patterns: [
|
|
16538
|
+
/\b(?:has no property|no property named)\b[\s:=]*['"`]?([A-Za-z_$][\w$]*)['"`]?/i
|
|
16539
|
+
]
|
|
16540
|
+
},
|
|
16541
|
+
{
|
|
16542
|
+
// "no such column: X", "unknown column 'X'", "column X does not exist".
|
|
16543
|
+
kind: "missing-column",
|
|
16544
|
+
patterns: [
|
|
16545
|
+
/\bno such column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
16546
|
+
/\bunknown column\b[\s:=]*['"`]?([\w$.]+)['"`]?/i,
|
|
16547
|
+
/\bcolumn\b[\s:=]*['"`]?([\w$.]+)['"`]?\s+does not exist/i
|
|
16548
|
+
]
|
|
16549
|
+
},
|
|
16550
|
+
{
|
|
16551
|
+
// "undefined method `foo' for X" — kept to the unambiguous form so a generic
|
|
16552
|
+
// "method not found" (an unimplemented RPC — an edge/route gap, not a symbol
|
|
16553
|
+
// mismatch) does not get miscategorised here.
|
|
16554
|
+
kind: "undefined-method",
|
|
16555
|
+
patterns: [/\bundefined method\b\s*[`'"]?([\w$?!]+)/i]
|
|
16556
|
+
}
|
|
16557
|
+
];
|
|
16558
|
+
function classifySymbolMismatch(message) {
|
|
16559
|
+
for (const entry2 of SYMBOL_MISMATCH_PATTERNS) {
|
|
16560
|
+
for (const re of entry2.patterns) {
|
|
16561
|
+
const m = re.exec(message);
|
|
16562
|
+
if (m) {
|
|
16563
|
+
const captured = m[1];
|
|
16564
|
+
return captured ? { kind: entry2.kind, symbol: captured } : { kind: entry2.kind };
|
|
16565
|
+
}
|
|
16566
|
+
}
|
|
16567
|
+
}
|
|
16568
|
+
return null;
|
|
16569
|
+
}
|
|
16570
|
+
function symbolLocus(graph, ev) {
|
|
16571
|
+
const attrs = ev.attributes ?? {};
|
|
16572
|
+
const filepath = codeFilepathOf(attrs);
|
|
16573
|
+
const lineno = codeLinenoOf(attrs);
|
|
16574
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
16575
|
+
const affected = ev.affectedNode;
|
|
16576
|
+
const affectedInGraph = affected.length > 0 && graph.hasNode(affected);
|
|
16577
|
+
const affectedIsCode = affectedInGraph && ((0, import_types58.parseSymbolId)(affected) !== null || (0, import_types58.parseFileId)(affected) !== null);
|
|
16578
|
+
if (affectedIsCode) return { node: affected, ...location ? { location } : {} };
|
|
16579
|
+
if (!location) return null;
|
|
16580
|
+
if (affectedInGraph) return { node: affected, location };
|
|
16581
|
+
const svc = (0, import_types58.serviceId)(ev.service);
|
|
16582
|
+
if (graph.hasNode(svc)) return { node: svc, location };
|
|
16583
|
+
return null;
|
|
16584
|
+
}
|
|
16585
|
+
var SYMBOL_MISMATCH_CONFIDENCE = 0.6;
|
|
16586
|
+
function detectSymbolMismatches(graph, incidents) {
|
|
16587
|
+
const groups = /* @__PURE__ */ new Map();
|
|
16588
|
+
for (const ev of incidents) {
|
|
16589
|
+
const classified = classifySymbolMismatch(ev.errorMessage);
|
|
16590
|
+
if (!classified) continue;
|
|
16591
|
+
const locus = symbolLocus(graph, ev);
|
|
16592
|
+
if (!locus) continue;
|
|
16593
|
+
const key = `${locus.node}|${classified.kind}|${classified.symbol ?? ""}`;
|
|
16594
|
+
const existing = groups.get(key);
|
|
16595
|
+
if (!existing) {
|
|
16596
|
+
groups.set(key, {
|
|
16597
|
+
node: locus.node,
|
|
16598
|
+
kind: classified.kind,
|
|
16599
|
+
...classified.symbol ? { symbol: classified.symbol } : {},
|
|
16600
|
+
...locus.location ? { location: locus.location } : {},
|
|
16601
|
+
latest: ev,
|
|
16602
|
+
count: 1
|
|
16603
|
+
});
|
|
16604
|
+
} else {
|
|
16605
|
+
existing.count += 1;
|
|
16606
|
+
if (ev.timestamp.localeCompare(existing.latest.timestamp) > 0) {
|
|
16607
|
+
existing.latest = ev;
|
|
16608
|
+
if (locus.location) existing.location = locus.location;
|
|
16609
|
+
}
|
|
16610
|
+
}
|
|
16611
|
+
}
|
|
16612
|
+
const out = [];
|
|
16613
|
+
for (const g of groups.values()) {
|
|
16614
|
+
const member = g.symbol ? `\`${g.symbol}\`` : "a member";
|
|
16615
|
+
const where = g.location ? ` at ${g.location}` : "";
|
|
16616
|
+
const times = g.count > 1 ? ` (${g.count} recorded incidents)` : " (1 recorded incident)";
|
|
16617
|
+
out.push({
|
|
16618
|
+
type: "observed-symbol-mismatch",
|
|
16619
|
+
source: g.node,
|
|
16620
|
+
target: g.node,
|
|
16621
|
+
mismatchKind: g.kind,
|
|
16622
|
+
...g.symbol ? { symbol: g.symbol } : {},
|
|
16623
|
+
...g.location ? { location: g.location } : {},
|
|
16624
|
+
provenance: import_types58.Provenance.INFERRED,
|
|
16625
|
+
incidentId: g.latest.id,
|
|
16626
|
+
errorMessage: g.latest.errorMessage,
|
|
16627
|
+
incidentCount: g.count,
|
|
16628
|
+
confidence: SYMBOL_MISMATCH_CONFIDENCE,
|
|
16629
|
+
reason: `Code${where} declares access to ${member} the runtime object does not have \u2014 ${g.latest.service} raised "${g.latest.errorMessage}"${times}. The declared access (EXTRACTED) and the runtime shape (OBSERVED) disagree at symbol grain.`,
|
|
16630
|
+
recommendation: "Reconcile the declared access with the runtime shape: a field, attribute, method, or column was renamed, removed, or never existed on the object this code reaches. Update the code to the current shape, or restore the member."
|
|
16631
|
+
});
|
|
16632
|
+
}
|
|
16633
|
+
return out;
|
|
16634
|
+
}
|
|
16455
16635
|
function involvesNode(d, nodeId) {
|
|
16456
16636
|
return d.source === nodeId || d.target === nodeId;
|
|
16457
16637
|
}
|
|
16638
|
+
function datastoreEverObserved(graph, nodeId) {
|
|
16639
|
+
if (!graph.hasNode(nodeId)) return false;
|
|
16640
|
+
const n = graph.getNodeAttributes(nodeId);
|
|
16641
|
+
if (n.type === import_types58.NodeType.DatabaseNode) {
|
|
16642
|
+
const via = n.discoveredVia;
|
|
16643
|
+
if (via === "otel" || via === "merged") return true;
|
|
16644
|
+
}
|
|
16645
|
+
for (const edgeId of graph.inboundEdges(nodeId)) {
|
|
16646
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
16647
|
+
if (e.provenance === import_types58.Provenance.OBSERVED || e.provenance === import_types58.Provenance.STALE) return true;
|
|
16648
|
+
}
|
|
16649
|
+
return false;
|
|
16650
|
+
}
|
|
16651
|
+
function serviceHasObservedSameEngineStore(graph, buckets2, serviceId16, engine, excludeTarget) {
|
|
16652
|
+
for (const bucket of buckets2.values()) {
|
|
16653
|
+
if (bucket.type !== import_types58.EdgeType.CONNECTS_TO) continue;
|
|
16654
|
+
if (bucket.source !== serviceId16) continue;
|
|
16655
|
+
if (bucket.target === excludeTarget) continue;
|
|
16656
|
+
if (!graph.hasNode(bucket.target)) continue;
|
|
16657
|
+
const target = graph.getNodeAttributes(bucket.target);
|
|
16658
|
+
if (target.type !== import_types58.NodeType.DatabaseNode) continue;
|
|
16659
|
+
if (target.engine !== engine) continue;
|
|
16660
|
+
if (bucket.observed || datastoreEverObserved(graph, bucket.target)) return true;
|
|
16661
|
+
}
|
|
16662
|
+
return false;
|
|
16663
|
+
}
|
|
16664
|
+
var DEAD_CODE_PROBE_CONFIDENCE = 0.1;
|
|
16665
|
+
function dampenDeadCodeProbes(graph, buckets2, all) {
|
|
16666
|
+
return all.map((d) => {
|
|
16667
|
+
if (d.type !== "missing-observed") return d;
|
|
16668
|
+
if (!d.extracted || d.edgeType !== import_types58.EdgeType.CONNECTS_TO) return d;
|
|
16669
|
+
if (!graph.hasNode(d.target)) return d;
|
|
16670
|
+
const target = graph.getNodeAttributes(d.target);
|
|
16671
|
+
if (target.type !== import_types58.NodeType.DatabaseNode) return d;
|
|
16672
|
+
if (d.extracted.evidence?.hostSource !== "literal") return d;
|
|
16673
|
+
if (datastoreEverObserved(graph, d.target)) return d;
|
|
16674
|
+
const engine = target.engine;
|
|
16675
|
+
if (!serviceHasObservedSameEngineStore(graph, buckets2, d.source, engine, d.target)) return d;
|
|
16676
|
+
const host = target.host ?? target.name;
|
|
16677
|
+
return {
|
|
16678
|
+
...d,
|
|
16679
|
+
confidence: Math.min(d.confidence, DEAD_CODE_PROBE_CONFIDENCE),
|
|
16680
|
+
reason: `${d.source} declares a ${engine} connection to a hardcoded-literal host (${host}) that production has never observed, while it does observe another ${engine} store \u2014 this reads as a flag-gated or dead-code declaration (e.g. a fault-injection probe), not a real declared-vs-observed gap.`,
|
|
16681
|
+
recommendation: "Confirm this is an intentional dead alternate \u2014 a fault-injection probe or a flag-gated branch. If it is meant to run in production, check the feature flag or conditional that gates it; if not, it can be ignored or removed."
|
|
16682
|
+
};
|
|
16683
|
+
});
|
|
16684
|
+
}
|
|
16458
16685
|
function suppressHostMismatchHalves(all) {
|
|
16459
16686
|
const observedHalf = /* @__PURE__ */ new Set();
|
|
16460
16687
|
const declaredHalf = /* @__PURE__ */ new Set();
|
|
@@ -16490,8 +16717,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16490
16717
|
for (const d of detectColumnDrift(n)) all.push(d);
|
|
16491
16718
|
}
|
|
16492
16719
|
});
|
|
16720
|
+
if (opts.incidents && opts.incidents.length > 0) {
|
|
16721
|
+
for (const d of detectSymbolMismatches(graph, opts.incidents)) all.push(d);
|
|
16722
|
+
}
|
|
16493
16723
|
const reconciled = suppressHostMismatchHalves(all);
|
|
16494
|
-
|
|
16724
|
+
const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
|
|
16725
|
+
let filtered = dampened;
|
|
16495
16726
|
if (opts.type) {
|
|
16496
16727
|
const allowed = opts.type;
|
|
16497
16728
|
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
@@ -16509,7 +16740,12 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16509
16740
|
"missing-observed": 1,
|
|
16510
16741
|
"version-mismatch": 2,
|
|
16511
16742
|
"host-mismatch": 3,
|
|
16512
|
-
"compat-violation": 4
|
|
16743
|
+
"compat-violation": 4,
|
|
16744
|
+
// Symbol/field-grain (ADR-215) rides the confidence sort like every other
|
|
16745
|
+
// type; this only breaks a confidence tie, and it orders last so a same-
|
|
16746
|
+
// confidence edge finding leads. In practice it carries the INFERRED grade
|
|
16747
|
+
// (0.6), so it sits below the high-confidence edge divergences already.
|
|
16748
|
+
"observed-symbol-mismatch": 5
|
|
16513
16749
|
};
|
|
16514
16750
|
filtered.sort((a, b) => {
|
|
16515
16751
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
@@ -16520,7 +16756,10 @@ function computeDivergences(graph, opts = {}) {
|
|
|
16520
16756
|
if (a.target !== b.target) return a.target.localeCompare(b.target);
|
|
16521
16757
|
const ac = "column" in a && a.column ? a.column : "";
|
|
16522
16758
|
const bc = "column" in b && b.column ? b.column : "";
|
|
16523
|
-
return ac.localeCompare(bc);
|
|
16759
|
+
if (ac !== bc) return ac.localeCompare(bc);
|
|
16760
|
+
const asym = "symbol" in a && a.symbol ? a.symbol : "";
|
|
16761
|
+
const bsym = "symbol" in b && b.symbol ? b.symbol : "";
|
|
16762
|
+
return asym.localeCompare(bsym);
|
|
16524
16763
|
});
|
|
16525
16764
|
return import_types58.DivergenceResultSchema.parse({
|
|
16526
16765
|
divergences: filtered,
|
|
@@ -16947,10 +17186,15 @@ function divergenceLine(d) {
|
|
|
16947
17186
|
if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
|
|
16948
17187
|
return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
|
|
16949
17188
|
}
|
|
17189
|
+
if (d.type === "observed-symbol-mismatch") {
|
|
17190
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
17191
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
17192
|
+
return `[${d.type}] ${d.source}${member}${at} \u2014 ${d.reason}`;
|
|
17193
|
+
}
|
|
16950
17194
|
return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
|
|
16951
17195
|
}
|
|
16952
|
-
function buildDivergenceSection(graph, node) {
|
|
16953
|
-
const result = computeDivergences(graph, { node });
|
|
17196
|
+
function buildDivergenceSection(graph, node, incidents) {
|
|
17197
|
+
const result = computeDivergences(graph, { node, ...incidents ? { incidents } : {} });
|
|
16954
17198
|
if (result.totalAffected === 0) return null;
|
|
16955
17199
|
const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
|
|
16956
17200
|
text: divergenceLine(d),
|
|
@@ -16963,8 +17207,8 @@ function buildDivergenceSection(graph, node) {
|
|
|
16963
17207
|
facts
|
|
16964
17208
|
};
|
|
16965
17209
|
}
|
|
16966
|
-
function buildGlobalDivergenceSection(graph) {
|
|
16967
|
-
const result = computeDivergences(graph);
|
|
17210
|
+
function buildGlobalDivergenceSection(graph, incidents) {
|
|
17211
|
+
const result = computeDivergences(graph, incidents ? { incidents } : {});
|
|
16968
17212
|
if (result.totalAffected === 0) {
|
|
16969
17213
|
return {
|
|
16970
17214
|
heading: "Divergences (EXTRACTED vs OBSERVED)",
|
|
@@ -17062,7 +17306,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
17062
17306
|
}))
|
|
17063
17307
|
});
|
|
17064
17308
|
}
|
|
17065
|
-
const div = computeDivergences(graph);
|
|
17309
|
+
const div = computeDivergences(graph, incidents ? { incidents } : {});
|
|
17066
17310
|
sections.push({
|
|
17067
17311
|
heading: "Divergences",
|
|
17068
17312
|
facts: [
|
|
@@ -17076,7 +17320,7 @@ function buildOverviewSections(graph, incidents) {
|
|
|
17076
17320
|
function buildGlobalSections(intent, graph, incidents) {
|
|
17077
17321
|
switch (intent) {
|
|
17078
17322
|
case "divergence":
|
|
17079
|
-
return [buildGlobalDivergenceSection(graph)];
|
|
17323
|
+
return [buildGlobalDivergenceSection(graph, incidents)];
|
|
17080
17324
|
case "incidents":
|
|
17081
17325
|
return [buildGlobalIncidentsSection(incidents)];
|
|
17082
17326
|
case "overview":
|
|
@@ -17107,7 +17351,7 @@ function buildSection(kind, graph, node, incidents, now) {
|
|
|
17107
17351
|
case "incidents":
|
|
17108
17352
|
return buildIncidentsSection(node, incidents);
|
|
17109
17353
|
case "divergence":
|
|
17110
|
-
return buildDivergenceSection(graph, node);
|
|
17354
|
+
return buildDivergenceSection(graph, node, incidents);
|
|
17111
17355
|
}
|
|
17112
17356
|
}
|
|
17113
17357
|
function summarizeGlobal(intent, sections) {
|
|
@@ -20982,6 +21226,8 @@ async function startConnectorPolling(input) {
|
|
|
20982
21226
|
}
|
|
20983
21227
|
|
|
20984
21228
|
// src/api.ts
|
|
21229
|
+
var INCIDENT_LIST_DEFAULT_LIMIT = 50;
|
|
21230
|
+
var INCIDENT_LIST_MAX_LIMIT = 200;
|
|
20985
21231
|
function serializeGraph(graph) {
|
|
20986
21232
|
const nodes = [];
|
|
20987
21233
|
graph.forEachNode((_id, attrs) => {
|
|
@@ -21161,10 +21407,13 @@ function registerRoutes(scope, ctx) {
|
|
|
21161
21407
|
}
|
|
21162
21408
|
minConfidence = n;
|
|
21163
21409
|
}
|
|
21410
|
+
const epath = errorsPathFor(proj);
|
|
21411
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
21164
21412
|
return computeDivergences(proj.graph, {
|
|
21165
21413
|
...typeFilter ? { type: typeFilter } : {},
|
|
21166
21414
|
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
21167
|
-
...req2.query.node ? { node: req2.query.node } : {}
|
|
21415
|
+
...req2.query.node ? { node: req2.query.node } : {},
|
|
21416
|
+
incidents
|
|
21168
21417
|
});
|
|
21169
21418
|
});
|
|
21170
21419
|
scope.get("/incidents", async (req2, reply) => {
|
|
@@ -21174,10 +21423,11 @@ function registerRoutes(scope, ctx) {
|
|
|
21174
21423
|
if (!epath) return { count: 0, total: 0, events: [] };
|
|
21175
21424
|
const events = await readErrorEvents(epath);
|
|
21176
21425
|
const total = events.length;
|
|
21177
|
-
const limit = req2.query.limit ? Number(req2.query.limit) :
|
|
21178
|
-
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit,
|
|
21426
|
+
const limit = req2.query.limit ? Number(req2.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21427
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21179
21428
|
const sliced = events.slice(0, safeLimit);
|
|
21180
|
-
|
|
21429
|
+
const omitted = total - sliced.length;
|
|
21430
|
+
return { count: sliced.length, total, events: sliced, ...omitted > 0 ? { omitted } : {} };
|
|
21181
21431
|
});
|
|
21182
21432
|
scope.get("/stale-events", async (req2, reply) => {
|
|
21183
21433
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -21286,16 +21536,20 @@ function registerRoutes(scope, ctx) {
|
|
|
21286
21536
|
const filtered = events.filter(
|
|
21287
21537
|
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
21288
21538
|
);
|
|
21289
|
-
|
|
21539
|
+
const total = filtered.length;
|
|
21540
|
+
const limit = req2.query.limit ? Number(req2.query.limit) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21541
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, INCIDENT_LIST_MAX_LIMIT) : INCIDENT_LIST_DEFAULT_LIMIT;
|
|
21542
|
+
const recent = [...filtered].sort((a, b) => (b.timestamp ?? "").localeCompare(a.timestamp ?? "")).slice(0, safeLimit);
|
|
21543
|
+
const omitted = total - recent.length;
|
|
21544
|
+
return {
|
|
21545
|
+
count: recent.length,
|
|
21546
|
+
total,
|
|
21547
|
+
events: recent,
|
|
21548
|
+
...omitted > 0 ? { omitted } : {}
|
|
21549
|
+
};
|
|
21290
21550
|
};
|
|
21291
|
-
scope.get(
|
|
21292
|
-
|
|
21293
|
-
incidentHistoryHandler
|
|
21294
|
-
);
|
|
21295
|
-
scope.get(
|
|
21296
|
-
"/graph/incident-history/:nodeId",
|
|
21297
|
-
incidentHistoryHandler
|
|
21298
|
-
);
|
|
21551
|
+
scope.get("/incidents/:nodeId", incidentHistoryHandler);
|
|
21552
|
+
scope.get("/graph/incident-history/:nodeId", incidentHistoryHandler);
|
|
21299
21553
|
scope.get("/graph/root-cause/:nodeId", async (req2, reply) => {
|
|
21300
21554
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
21301
21555
|
if (!proj) return;
|