@neat.is/core 0.6.1 → 0.6.2-dev.20260723
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-BZ3AJVAC.js → chunk-2MAGU6RB.js} +11 -2
- package/dist/chunk-2MAGU6RB.js.map +1 -0
- package/dist/{chunk-3YUNROLT.js → chunk-64JJOXES.js} +3 -3
- package/dist/{chunk-MVINCLQM.js → chunk-OY7FGJAX.js} +2 -2
- package/dist/{chunk-7C6ZULNU.js → chunk-ZZ3VUCWL.js} +1492 -1346
- package/dist/chunk-ZZ3VUCWL.js.map +1 -0
- package/dist/cli.cjs +783 -468
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +170 -11
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +466 -310
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +475 -319
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-XS45HBET.js → otel-grpc-AHNPM5LX.js} +3 -3
- package/dist/server.cjs +2400 -2240
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-7C6ZULNU.js.map +0 -1
- package/dist/chunk-BZ3AJVAC.js.map +0 -1
- /package/dist/{chunk-3YUNROLT.js.map → chunk-64JJOXES.js.map} +0 -0
- /package/dist/{chunk-MVINCLQM.js.map → chunk-OY7FGJAX.js.map} +0 -0
- /package/dist/{otel-grpc-XS45HBET.js.map → otel-grpc-AHNPM5LX.js.map} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
mountBearerAuth,
|
|
3
3
|
readAuthEnv
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2MAGU6RB.js";
|
|
5
5
|
|
|
6
6
|
// src/graph.ts
|
|
7
7
|
import GraphDefault from "graphology";
|
|
@@ -339,969 +339,454 @@ function deprecatedApis() {
|
|
|
339
339
|
return currentMatrix().deprecatedApis ?? [];
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
// src/
|
|
342
|
+
// src/ingest.ts
|
|
343
|
+
import { promises as fs3, existsSync, readFileSync } from "fs";
|
|
344
|
+
import path3 from "path";
|
|
345
|
+
import * as sourceMapJs from "source-map-js";
|
|
346
|
+
|
|
347
|
+
// src/policy.ts
|
|
348
|
+
import { promises as fs2 } from "fs";
|
|
349
|
+
import path2 from "path";
|
|
343
350
|
import {
|
|
344
|
-
BlastRadiusResultSchema,
|
|
345
351
|
EdgeType,
|
|
346
352
|
NodeType,
|
|
347
|
-
|
|
348
|
-
PROV_RANK,
|
|
349
|
-
Provenance,
|
|
350
|
-
RootCauseResultSchema,
|
|
351
|
-
TransitiveDependenciesResultSchema
|
|
353
|
+
PolicyFileSchema
|
|
352
354
|
} from "@neat.is/types";
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
return attrs.type === NodeType.FrontierNode;
|
|
359
|
-
}
|
|
360
|
-
function resolveOwningService(graph, nodeId) {
|
|
361
|
-
if (!graph.hasNode(nodeId)) return null;
|
|
362
|
-
const attrs = graph.getNodeAttributes(nodeId);
|
|
363
|
-
if (attrs.type === NodeType.ServiceNode) {
|
|
364
|
-
return { id: nodeId, svc: attrs };
|
|
365
|
-
}
|
|
366
|
-
if (attrs.type === NodeType.FileNode) {
|
|
367
|
-
for (const edgeId of graph.inboundEdges(nodeId)) {
|
|
368
|
-
const e = graph.getEdgeAttributes(edgeId);
|
|
369
|
-
if (e.type !== EdgeType.CONTAINS) continue;
|
|
370
|
-
const owner = graph.getNodeAttributes(e.source);
|
|
371
|
-
if (owner.type === NodeType.ServiceNode) {
|
|
372
|
-
return { id: e.source, svc: owner };
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
return null;
|
|
377
|
-
}
|
|
378
|
-
function bestEdgeBySource(graph, edgeIds) {
|
|
379
|
-
const best = /* @__PURE__ */ new Map();
|
|
380
|
-
for (const id of edgeIds) {
|
|
381
|
-
const e = graph.getEdgeAttributes(id);
|
|
382
|
-
if (isFrontierNode(graph, e.source)) continue;
|
|
383
|
-
const cur = best.get(e.source);
|
|
384
|
-
if (!cur || PROV_RANK[e.provenance] > PROV_RANK[cur.provenance]) {
|
|
385
|
-
best.set(e.source, e);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
return best;
|
|
389
|
-
}
|
|
390
|
-
function bestEdgeByTarget(graph, edgeIds) {
|
|
391
|
-
const best = /* @__PURE__ */ new Map();
|
|
392
|
-
for (const id of edgeIds) {
|
|
393
|
-
const e = graph.getEdgeAttributes(id);
|
|
394
|
-
if (isFrontierNode(graph, e.target)) continue;
|
|
395
|
-
const cur = best.get(e.target);
|
|
396
|
-
if (!cur || PROV_RANK[e.provenance] > PROV_RANK[cur.provenance]) {
|
|
397
|
-
best.set(e.target, e);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
return best;
|
|
401
|
-
}
|
|
402
|
-
var PROVENANCE_CEILING = {
|
|
403
|
-
OBSERVED: 1,
|
|
404
|
-
INFERRED: 0.7,
|
|
405
|
-
EXTRACTED: 0.5,
|
|
406
|
-
STALE: 0.3
|
|
355
|
+
|
|
356
|
+
// src/events.ts
|
|
357
|
+
import { EventEmitter } from "events";
|
|
358
|
+
var EVENT_BUS_CHANNEL = "event";
|
|
359
|
+
var NeatEventBus = class extends EventEmitter {
|
|
407
360
|
};
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
function recencyWeight(ageMs) {
|
|
414
|
-
if (ageMs === void 0) return 0.8;
|
|
415
|
-
const hour = 60 * 60 * 1e3;
|
|
416
|
-
if (ageMs <= hour) return 1;
|
|
417
|
-
if (ageMs <= 24 * hour) {
|
|
418
|
-
const t = (ageMs - hour) / (23 * hour);
|
|
419
|
-
return 1 - 0.5 * t;
|
|
420
|
-
}
|
|
421
|
-
return 0.3;
|
|
422
|
-
}
|
|
423
|
-
function cleanlinessWeight(spanCount, errorCount) {
|
|
424
|
-
if (!spanCount || spanCount <= 0) return 1;
|
|
425
|
-
const rate = (errorCount ?? 0) / spanCount;
|
|
426
|
-
if (rate <= 0.01) return 1;
|
|
427
|
-
if (rate >= 0.5) return 0.3;
|
|
428
|
-
return 1 - rate * 1.4;
|
|
429
|
-
}
|
|
430
|
-
function confidenceForEdge(edge, now = Date.now()) {
|
|
431
|
-
const ceiling = PROVENANCE_CEILING[edge.provenance] ?? 0.5;
|
|
432
|
-
const spanCount = edge.signal?.spanCount ?? edge.callCount;
|
|
433
|
-
const ageMs = edge.signal?.lastObservedAgeMs ?? lastObservedAge(edge, now);
|
|
434
|
-
if (spanCount === void 0 && ageMs === void 0 && edge.signal === void 0) {
|
|
435
|
-
return ceiling;
|
|
436
|
-
}
|
|
437
|
-
const v = volumeWeight(spanCount);
|
|
438
|
-
const r = recencyWeight(ageMs);
|
|
439
|
-
const c = cleanlinessWeight(spanCount, edge.signal?.errorCount);
|
|
440
|
-
return Math.max(0, Math.min(1, ceiling * v * r * c));
|
|
361
|
+
var eventBus = new NeatEventBus();
|
|
362
|
+
eventBus.setMaxListeners(0);
|
|
363
|
+
function emitNeatEvent(envelope) {
|
|
364
|
+
eventBus.emit(EVENT_BUS_CHANNEL, envelope);
|
|
441
365
|
}
|
|
442
|
-
function
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
366
|
+
function attachGraphToEventBus(graph, opts) {
|
|
367
|
+
const { project } = opts;
|
|
368
|
+
const onNodeAdded = (payload) => {
|
|
369
|
+
emitNeatEvent({
|
|
370
|
+
type: "node-added",
|
|
371
|
+
project,
|
|
372
|
+
payload: { node: payload.attributes }
|
|
373
|
+
});
|
|
374
|
+
};
|
|
375
|
+
const onNodeDropped = (payload) => {
|
|
376
|
+
emitNeatEvent({
|
|
377
|
+
type: "node-removed",
|
|
378
|
+
project,
|
|
379
|
+
payload: { id: payload.key }
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
const onEdgeAdded = (payload) => {
|
|
383
|
+
emitNeatEvent({
|
|
384
|
+
type: "edge-added",
|
|
385
|
+
project,
|
|
386
|
+
payload: { edge: payload.attributes }
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
const onEdgeDropped = (payload) => {
|
|
390
|
+
emitNeatEvent({
|
|
391
|
+
type: "edge-removed",
|
|
392
|
+
project,
|
|
393
|
+
payload: { id: payload.key }
|
|
394
|
+
});
|
|
395
|
+
};
|
|
396
|
+
const onNodeAttrsUpdated = (payload) => {
|
|
397
|
+
emitNeatEvent({
|
|
398
|
+
type: "node-updated",
|
|
399
|
+
project,
|
|
400
|
+
payload: { id: payload.key, changes: payload.attributes }
|
|
401
|
+
});
|
|
402
|
+
};
|
|
403
|
+
graph.on("nodeAdded", onNodeAdded);
|
|
404
|
+
graph.on("nodeDropped", onNodeDropped);
|
|
405
|
+
graph.on("edgeAdded", onEdgeAdded);
|
|
406
|
+
graph.on("edgeDropped", onEdgeDropped);
|
|
407
|
+
graph.on("nodeAttributesUpdated", onNodeAttrsUpdated);
|
|
408
|
+
return () => {
|
|
409
|
+
graph.off("nodeAdded", onNodeAdded);
|
|
410
|
+
graph.off("nodeDropped", onNodeDropped);
|
|
411
|
+
graph.off("edgeAdded", onEdgeAdded);
|
|
412
|
+
graph.off("edgeDropped", onEdgeDropped);
|
|
413
|
+
graph.off("nodeAttributesUpdated", onNodeAttrsUpdated);
|
|
414
|
+
};
|
|
447
415
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
416
|
+
|
|
417
|
+
// src/policy.ts
|
|
418
|
+
var DEFAULT_ACTION_BY_SEVERITY = {
|
|
419
|
+
info: "log",
|
|
420
|
+
warning: "alert",
|
|
421
|
+
error: "alert",
|
|
422
|
+
critical: "block"
|
|
423
|
+
};
|
|
424
|
+
function resolveOnViolation(policy) {
|
|
425
|
+
return policy.onViolation ?? DEFAULT_ACTION_BY_SEVERITY[policy.severity];
|
|
455
426
|
}
|
|
456
|
-
function
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
path48.push(srcId);
|
|
469
|
-
edges.push(edge);
|
|
470
|
-
step(srcId, path48, edges);
|
|
471
|
-
path48.pop();
|
|
472
|
-
edges.pop();
|
|
473
|
-
visited.delete(srcId);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
step(start, [start], []);
|
|
477
|
-
return best;
|
|
427
|
+
function makeViolation(policy, rule, contextSuffix, message, subject, ctx) {
|
|
428
|
+
return {
|
|
429
|
+
id: `${policy.id}:${contextSuffix}`,
|
|
430
|
+
policyId: policy.id,
|
|
431
|
+
policyName: policy.name,
|
|
432
|
+
severity: policy.severity,
|
|
433
|
+
onViolation: resolveOnViolation(policy),
|
|
434
|
+
ruleType: rule.type,
|
|
435
|
+
subject,
|
|
436
|
+
message,
|
|
437
|
+
observedAt: new Date(ctx.now()).toISOString()
|
|
438
|
+
};
|
|
478
439
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
return {
|
|
499
|
-
rootCauseNode: serviceId5,
|
|
500
|
-
rootCauseReason: result.reason ?? "incompatible driver",
|
|
501
|
-
...result.minDriverVersion ? {
|
|
502
|
-
fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
|
|
503
|
-
} : {}
|
|
504
|
-
};
|
|
440
|
+
var evaluateStructural = ({
|
|
441
|
+
graph,
|
|
442
|
+
policy,
|
|
443
|
+
rule,
|
|
444
|
+
ctx
|
|
445
|
+
}) => {
|
|
446
|
+
const violations = [];
|
|
447
|
+
graph.forEachNode((id, attrs) => {
|
|
448
|
+
const a = attrs;
|
|
449
|
+
if (a.type !== rule.fromNodeType) return;
|
|
450
|
+
let satisfied = false;
|
|
451
|
+
for (const edgeId of graph.outboundEdges(id)) {
|
|
452
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
453
|
+
if (e.type !== rule.edgeType) continue;
|
|
454
|
+
const target = graph.getNodeAttributes(e.target);
|
|
455
|
+
if (target.type === NodeType.FrontierNode) continue;
|
|
456
|
+
if (target.type === rule.toNodeType) {
|
|
457
|
+
satisfied = true;
|
|
458
|
+
break;
|
|
505
459
|
}
|
|
506
460
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
const declared = deps[constraint.package];
|
|
519
|
-
if (!declared) continue;
|
|
520
|
-
const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
|
|
521
|
-
if (!result.compatible && result.reason) {
|
|
522
|
-
return {
|
|
523
|
-
rootCauseNode: serviceId5,
|
|
524
|
-
rootCauseReason: result.reason,
|
|
525
|
-
...result.requiredNodeVersion ? {
|
|
526
|
-
fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
|
|
527
|
-
} : {}
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
for (const conflict of packageConflicts()) {
|
|
532
|
-
const declared = deps[conflict.package];
|
|
533
|
-
if (!declared) continue;
|
|
534
|
-
const requiredDeclared = deps[conflict.requires.name];
|
|
535
|
-
const result = checkPackageConflict(conflict, declared, requiredDeclared);
|
|
536
|
-
if (!result.compatible && result.reason) {
|
|
537
|
-
return {
|
|
538
|
-
rootCauseNode: serviceId5,
|
|
539
|
-
rootCauseReason: result.reason,
|
|
540
|
-
fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
|
|
541
|
-
};
|
|
542
|
-
}
|
|
461
|
+
if (!satisfied) {
|
|
462
|
+
violations.push(
|
|
463
|
+
makeViolation(
|
|
464
|
+
policy,
|
|
465
|
+
rule,
|
|
466
|
+
id,
|
|
467
|
+
`${rule.fromNodeType} ${id} has no ${rule.edgeType} edge to a ${rule.toNodeType}`,
|
|
468
|
+
{ nodeId: id },
|
|
469
|
+
ctx
|
|
470
|
+
)
|
|
471
|
+
);
|
|
543
472
|
}
|
|
544
|
-
}
|
|
545
|
-
return
|
|
546
|
-
}
|
|
547
|
-
function fileRootCauseShape(graph, origin, walk3) {
|
|
548
|
-
const owner = resolveOwningService(graph, origin.id);
|
|
549
|
-
if (!owner) return null;
|
|
550
|
-
return serviceRootCauseShape(graph, owner.svc, walk3);
|
|
551
|
-
}
|
|
552
|
-
var rootCauseShapes = {
|
|
553
|
-
[NodeType.DatabaseNode]: databaseRootCauseShape,
|
|
554
|
-
[NodeType.ServiceNode]: serviceRootCauseShape,
|
|
555
|
-
[NodeType.FileNode]: fileRootCauseShape
|
|
473
|
+
});
|
|
474
|
+
return violations;
|
|
556
475
|
};
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
476
|
+
var evaluateOwnership = ({
|
|
477
|
+
graph,
|
|
478
|
+
policy,
|
|
479
|
+
rule,
|
|
480
|
+
ctx
|
|
481
|
+
}) => {
|
|
482
|
+
const violations = [];
|
|
483
|
+
graph.forEachNode((id, attrs) => {
|
|
484
|
+
const a = attrs;
|
|
485
|
+
if (a.type !== rule.nodeType) return;
|
|
486
|
+
const value = a[rule.field];
|
|
487
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
488
|
+
violations.push(
|
|
489
|
+
makeViolation(
|
|
490
|
+
policy,
|
|
491
|
+
rule,
|
|
492
|
+
id,
|
|
493
|
+
`${rule.nodeType} ${id} is missing required field "${rule.field}"`,
|
|
494
|
+
{ nodeId: id },
|
|
495
|
+
ctx
|
|
496
|
+
)
|
|
497
|
+
);
|
|
574
498
|
}
|
|
575
|
-
}
|
|
576
|
-
if (origin.type === NodeType.ServiceNode) {
|
|
577
|
-
const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
|
|
578
|
-
if (crossService) return crossService;
|
|
579
|
-
}
|
|
580
|
-
return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
|
|
581
|
-
}
|
|
582
|
-
var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
|
|
583
|
-
function incidentMatchesNode(ev, nodeId) {
|
|
584
|
-
return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
|
|
585
|
-
}
|
|
586
|
-
function localizeFromIncidents(nodeId, incidents, errorEvent) {
|
|
587
|
-
const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
|
|
588
|
-
const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
|
|
589
|
-
if (relevant.length === 0) return null;
|
|
590
|
-
const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
|
|
591
|
-
const attrs = latest.attributes ?? {};
|
|
592
|
-
const filepath = typeof attrs["code.filepath"] === "string" ? attrs["code.filepath"] : void 0;
|
|
593
|
-
const lineno = typeof attrs["code.lineno"] === "number" ? attrs["code.lineno"] : void 0;
|
|
594
|
-
const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
|
|
595
|
-
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
596
|
-
const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
|
|
597
|
-
const count = sameMode.length;
|
|
598
|
-
const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
|
|
599
|
-
const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
|
|
600
|
-
if (location) reasonParts.push(`surfaced at ${location}`);
|
|
601
|
-
const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
|
|
602
|
-
const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
|
|
603
|
-
const fileNode = localizesToFile ? latest.affectedNode : void 0;
|
|
604
|
-
const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
|
|
605
|
-
return {
|
|
606
|
-
rootCauseNode: fileNode ?? nodeId,
|
|
607
|
-
rootCauseReason,
|
|
608
|
-
...fileNode ? { fileNode } : {},
|
|
609
|
-
...fixRecommendation ? { fixRecommendation } : {}
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
613
|
-
const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
|
|
614
|
-
if (!loc) return null;
|
|
615
|
-
const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
|
|
616
|
-
const edgeProvenances = loc.fileNode ? [Provenance.OBSERVED] : [];
|
|
617
|
-
return RootCauseResultSchema.parse({
|
|
618
|
-
rootCauseNode: loc.rootCauseNode,
|
|
619
|
-
rootCauseReason: loc.rootCauseReason,
|
|
620
|
-
traversalPath,
|
|
621
|
-
edgeProvenances,
|
|
622
|
-
confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
|
|
623
|
-
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
624
499
|
});
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
for (const edgeId of graph.outboundEdges(src)) {
|
|
652
|
-
const e = graph.getEdgeAttributes(edgeId);
|
|
653
|
-
if (!isFailingCallEdge(e)) continue;
|
|
654
|
-
if (isFrontierNode(graph, e.target)) continue;
|
|
655
|
-
const owner = resolveOwningService(graph, e.target);
|
|
656
|
-
if (!owner || visited.has(owner.id)) continue;
|
|
657
|
-
if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
|
|
658
|
-
best = { nextService: owner.id, edge: e };
|
|
659
|
-
}
|
|
500
|
+
return violations;
|
|
501
|
+
};
|
|
502
|
+
var evaluateProvenance = ({
|
|
503
|
+
graph,
|
|
504
|
+
policy,
|
|
505
|
+
rule,
|
|
506
|
+
ctx
|
|
507
|
+
}) => {
|
|
508
|
+
const required = Array.isArray(rule.required) ? new Set(rule.required) : /* @__PURE__ */ new Set([rule.required]);
|
|
509
|
+
const violations = [];
|
|
510
|
+
graph.forEachEdge((edgeId, attrs) => {
|
|
511
|
+
const e = attrs;
|
|
512
|
+
if (e.type !== rule.edgeType) return;
|
|
513
|
+
if (rule.targetNodeId && e.target !== rule.targetNodeId) return;
|
|
514
|
+
if (!required.has(e.provenance)) {
|
|
515
|
+
const requiredList = [...required].join(" | ");
|
|
516
|
+
violations.push(
|
|
517
|
+
makeViolation(
|
|
518
|
+
policy,
|
|
519
|
+
rule,
|
|
520
|
+
edgeId,
|
|
521
|
+
`${rule.edgeType} edge ${edgeId} has provenance ${e.provenance}; required ${requiredList}`,
|
|
522
|
+
{ edgeId },
|
|
523
|
+
ctx
|
|
524
|
+
)
|
|
525
|
+
);
|
|
660
526
|
}
|
|
661
|
-
}
|
|
662
|
-
return
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
688
|
-
const loc = localizeFromIncidents(culprit, incidents, errorEvent);
|
|
689
|
-
if (loc) {
|
|
690
|
-
let rootCauseNode = culprit;
|
|
691
|
-
if (loc.fileNode) {
|
|
692
|
-
path48.push(loc.fileNode);
|
|
693
|
-
edgeProvenances.push(Provenance.OBSERVED);
|
|
694
|
-
rootCauseNode = loc.fileNode;
|
|
527
|
+
});
|
|
528
|
+
return violations;
|
|
529
|
+
};
|
|
530
|
+
var evaluateBlastRadius = ({
|
|
531
|
+
graph,
|
|
532
|
+
policy,
|
|
533
|
+
rule,
|
|
534
|
+
ctx
|
|
535
|
+
}) => {
|
|
536
|
+
const violations = [];
|
|
537
|
+
const depth = rule.depth;
|
|
538
|
+
graph.forEachNode((id, attrs) => {
|
|
539
|
+
const a = attrs;
|
|
540
|
+
if (a.type !== rule.nodeType) return;
|
|
541
|
+
const result = depth !== void 0 ? getBlastRadius(graph, id, depth) : getBlastRadius(graph, id);
|
|
542
|
+
if (result.totalAffected > rule.maxAffected) {
|
|
543
|
+
violations.push(
|
|
544
|
+
makeViolation(
|
|
545
|
+
policy,
|
|
546
|
+
rule,
|
|
547
|
+
id,
|
|
548
|
+
`${rule.nodeType} ${id} has blast radius ${result.totalAffected} > ${rule.maxAffected}`,
|
|
549
|
+
{ nodeId: id, path: [id] },
|
|
550
|
+
ctx
|
|
551
|
+
)
|
|
552
|
+
);
|
|
695
553
|
}
|
|
696
|
-
return RootCauseResultSchema.parse({
|
|
697
|
-
rootCauseNode,
|
|
698
|
-
rootCauseReason: loc.rootCauseReason,
|
|
699
|
-
traversalPath: path48,
|
|
700
|
-
edgeProvenances,
|
|
701
|
-
confidence,
|
|
702
|
-
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
703
|
-
});
|
|
704
|
-
}
|
|
705
|
-
const lastEdge = chain.edges[chain.edges.length - 1];
|
|
706
|
-
const errs = lastEdge.signal?.errorCount ?? 0;
|
|
707
|
-
const culpritName = culprit.replace(/^service:/, "");
|
|
708
|
-
return RootCauseResultSchema.parse({
|
|
709
|
-
rootCauseNode: culprit,
|
|
710
|
-
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
711
|
-
traversalPath: path48,
|
|
712
|
-
edgeProvenances,
|
|
713
|
-
confidence,
|
|
714
|
-
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
715
554
|
});
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
}
|
|
758
|
-
var TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH = 3;
|
|
759
|
-
var TRANSITIVE_DEPENDENCIES_MAX_DEPTH = 10;
|
|
760
|
-
function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH) {
|
|
761
|
-
if (!graph.hasNode(nodeId)) {
|
|
762
|
-
return TransitiveDependenciesResultSchema.parse({
|
|
763
|
-
origin: nodeId,
|
|
764
|
-
depth,
|
|
765
|
-
dependencies: [],
|
|
766
|
-
total: 0
|
|
767
|
-
});
|
|
768
|
-
}
|
|
769
|
-
const seen = /* @__PURE__ */ new Map();
|
|
770
|
-
const queue = [{ nodeId, distance: 0, edge: null }];
|
|
771
|
-
const enqueued = /* @__PURE__ */ new Set([nodeId]);
|
|
772
|
-
while (queue.length > 0) {
|
|
773
|
-
const frame = queue.shift();
|
|
774
|
-
if (frame.distance > 0 && frame.edge && frame.edge.type !== EdgeType.CONTAINS) {
|
|
775
|
-
seen.set(frame.nodeId, {
|
|
776
|
-
nodeId: frame.nodeId,
|
|
777
|
-
distance: frame.distance,
|
|
778
|
-
edgeType: frame.edge.type,
|
|
779
|
-
provenance: frame.edge.provenance
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
|
-
if (frame.distance >= depth) continue;
|
|
783
|
-
const outgoing = bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
|
|
784
|
-
for (const [tgtId, edge] of outgoing) {
|
|
785
|
-
if (enqueued.has(tgtId)) continue;
|
|
786
|
-
enqueued.add(tgtId);
|
|
787
|
-
queue.push({ nodeId: tgtId, distance: frame.distance + 1, edge });
|
|
555
|
+
return violations;
|
|
556
|
+
};
|
|
557
|
+
var evaluateCompatibility = ({
|
|
558
|
+
graph,
|
|
559
|
+
policy,
|
|
560
|
+
rule,
|
|
561
|
+
ctx
|
|
562
|
+
}) => {
|
|
563
|
+
const violations = [];
|
|
564
|
+
const wantsKind = (kind) => rule.kind === void 0 || rule.kind === kind;
|
|
565
|
+
graph.forEachNode((svcId, attrs) => {
|
|
566
|
+
const a = attrs;
|
|
567
|
+
if (a.type !== NodeType.ServiceNode) return;
|
|
568
|
+
const svc = a;
|
|
569
|
+
const deps = svc.dependencies ?? {};
|
|
570
|
+
if (wantsKind("driver-engine")) {
|
|
571
|
+
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
572
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
573
|
+
if (e.type !== EdgeType.CONNECTS_TO) continue;
|
|
574
|
+
const dbAttrs = graph.getNodeAttributes(e.target);
|
|
575
|
+
if (dbAttrs.type === NodeType.FrontierNode) continue;
|
|
576
|
+
if (dbAttrs.type !== NodeType.DatabaseNode) continue;
|
|
577
|
+
const db = dbAttrs;
|
|
578
|
+
for (const pair of compatPairs()) {
|
|
579
|
+
if (pair.engine !== db.engine) continue;
|
|
580
|
+
const declared = deps[pair.driver];
|
|
581
|
+
if (!declared) continue;
|
|
582
|
+
const result = checkCompatibility(pair.driver, declared, db.engine, db.engineVersion);
|
|
583
|
+
if (!result.compatible && result.reason) {
|
|
584
|
+
violations.push(
|
|
585
|
+
makeViolation(
|
|
586
|
+
policy,
|
|
587
|
+
rule,
|
|
588
|
+
`${svcId}:driver-engine:${pair.driver}@${declared}:${db.engine}@${db.engineVersion}`,
|
|
589
|
+
result.reason,
|
|
590
|
+
{ nodeId: svcId, edgeId },
|
|
591
|
+
ctx
|
|
592
|
+
)
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
788
597
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
const attrs = graph.getNodeAttributes(nodeId);
|
|
811
|
-
const scope = [nodeId];
|
|
812
|
-
if (attrs.type === NodeType.ServiceNode) {
|
|
813
|
-
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
814
|
-
const e = graph.getEdgeAttributes(edgeId);
|
|
815
|
-
if (e.type !== EdgeType.CONTAINS) continue;
|
|
816
|
-
const owned = graph.getNodeAttributes(e.target);
|
|
817
|
-
if (owned.type === NodeType.FileNode) scope.push(e.target);
|
|
598
|
+
if (wantsKind("node-engine")) {
|
|
599
|
+
const serviceNodeRange = svc.nodeEngine;
|
|
600
|
+
for (const constraint of nodeEngineConstraints()) {
|
|
601
|
+
const declared = deps[constraint.package];
|
|
602
|
+
if (!declared) continue;
|
|
603
|
+
const result = checkNodeEngineConstraint(constraint, declared, serviceNodeRange);
|
|
604
|
+
if (!result.compatible && result.reason) {
|
|
605
|
+
violations.push(
|
|
606
|
+
makeViolation(
|
|
607
|
+
policy,
|
|
608
|
+
rule,
|
|
609
|
+
`${svcId}:node-engine:${constraint.package}@${declared}`,
|
|
610
|
+
result.reason,
|
|
611
|
+
{ nodeId: svcId },
|
|
612
|
+
ctx
|
|
613
|
+
)
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
818
617
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
618
|
+
if (wantsKind("package-conflict")) {
|
|
619
|
+
for (const conflict of packageConflicts()) {
|
|
620
|
+
const declared = deps[conflict.package];
|
|
621
|
+
if (!declared) continue;
|
|
622
|
+
const requiredDeclared = deps[conflict.requires.name];
|
|
623
|
+
const result = checkPackageConflict(conflict, declared, requiredDeclared);
|
|
624
|
+
if (!result.compatible && result.reason) {
|
|
625
|
+
violations.push(
|
|
626
|
+
makeViolation(
|
|
627
|
+
policy,
|
|
628
|
+
rule,
|
|
629
|
+
`${svcId}:package-conflict:${conflict.package}@${declared}`,
|
|
630
|
+
result.reason,
|
|
631
|
+
{ nodeId: svcId },
|
|
632
|
+
ctx
|
|
633
|
+
)
|
|
634
|
+
);
|
|
831
635
|
}
|
|
832
|
-
} else if (e.provenance === Provenance.EXTRACTED) {
|
|
833
|
-
hasExtractedOutbound = true;
|
|
834
636
|
}
|
|
835
637
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
638
|
+
if (wantsKind("deprecated-api")) {
|
|
639
|
+
for (const dep of deprecatedApis()) {
|
|
640
|
+
const declared = deps[dep.package];
|
|
641
|
+
if (!declared) continue;
|
|
642
|
+
const result = checkDeprecatedApi(dep, declared);
|
|
643
|
+
if (!result.compatible && result.reason) {
|
|
644
|
+
violations.push(
|
|
645
|
+
makeViolation(
|
|
646
|
+
policy,
|
|
647
|
+
rule,
|
|
648
|
+
`${svcId}:deprecated-api:${dep.package}@${declared}`,
|
|
649
|
+
result.reason,
|
|
650
|
+
{ nodeId: svcId },
|
|
651
|
+
ctx
|
|
652
|
+
)
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
843
656
|
}
|
|
844
|
-
}
|
|
845
|
-
dependencies.sort(
|
|
846
|
-
(a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
|
|
847
|
-
);
|
|
848
|
-
return ObservedDependenciesResultSchema.parse({
|
|
849
|
-
origin: nodeId,
|
|
850
|
-
dependencies,
|
|
851
|
-
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
852
|
-
inboundObservedCount,
|
|
853
|
-
hasExtractedOutbound
|
|
854
657
|
});
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
// src/ingest.ts
|
|
858
|
-
import { promises as fs3, existsSync, readFileSync } from "fs";
|
|
859
|
-
import path3 from "path";
|
|
860
|
-
import * as sourceMapJs from "source-map-js";
|
|
861
|
-
|
|
862
|
-
// src/policy.ts
|
|
863
|
-
import { promises as fs2 } from "fs";
|
|
864
|
-
import path2 from "path";
|
|
865
|
-
import {
|
|
866
|
-
EdgeType as EdgeType2,
|
|
867
|
-
NodeType as NodeType2,
|
|
868
|
-
PolicyFileSchema
|
|
869
|
-
} from "@neat.is/types";
|
|
870
|
-
|
|
871
|
-
// src/events.ts
|
|
872
|
-
import { EventEmitter } from "events";
|
|
873
|
-
var EVENT_BUS_CHANNEL = "event";
|
|
874
|
-
var NeatEventBus = class extends EventEmitter {
|
|
658
|
+
return violations;
|
|
875
659
|
};
|
|
876
|
-
var
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
660
|
+
var policyEvaluators = {
|
|
661
|
+
structural: evaluateStructural,
|
|
662
|
+
ownership: evaluateOwnership,
|
|
663
|
+
provenance: evaluateProvenance,
|
|
664
|
+
"blast-radius": evaluateBlastRadius,
|
|
665
|
+
compatibility: evaluateCompatibility
|
|
666
|
+
};
|
|
667
|
+
function canPromoteFrontier(graph, frontierId2, policies, ctx) {
|
|
668
|
+
if (policies.length === 0) return { allowed: true, violations: [] };
|
|
669
|
+
const all = evaluateAllPolicies(graph, policies, ctx);
|
|
670
|
+
const blocking = all.filter((v) => {
|
|
671
|
+
if (v.onViolation !== "block") return false;
|
|
672
|
+
return v.subject.nodeId === frontierId2 || v.subject.path?.includes(frontierId2) === true;
|
|
673
|
+
});
|
|
674
|
+
return { allowed: blocking.length === 0, violations: blocking };
|
|
880
675
|
}
|
|
881
|
-
function
|
|
882
|
-
const
|
|
883
|
-
const
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
type: "edge-removed",
|
|
907
|
-
project,
|
|
908
|
-
payload: { id: payload.key }
|
|
909
|
-
});
|
|
910
|
-
};
|
|
911
|
-
const onNodeAttrsUpdated = (payload) => {
|
|
912
|
-
emitNeatEvent({
|
|
913
|
-
type: "node-updated",
|
|
914
|
-
project,
|
|
915
|
-
payload: { id: payload.key, changes: payload.attributes }
|
|
676
|
+
function evaluateAllPolicies(graph, policies, ctx) {
|
|
677
|
+
const out = [];
|
|
678
|
+
for (const policy of policies) {
|
|
679
|
+
const evaluator = policyEvaluators[policy.rule.type];
|
|
680
|
+
const violations = evaluator({ graph, policy, rule: policy.rule, ctx });
|
|
681
|
+
for (const v of violations) out.push(v);
|
|
682
|
+
}
|
|
683
|
+
return out;
|
|
684
|
+
}
|
|
685
|
+
function selectApplicablePolicies(graph, policies, nodeId) {
|
|
686
|
+
if (!graph.hasNode(nodeId)) return [];
|
|
687
|
+
const node = graph.getNodeAttributes(nodeId);
|
|
688
|
+
const out = [];
|
|
689
|
+
for (const policy of policies) {
|
|
690
|
+
const m = matchPolicyToNode(graph, policy, nodeId, node);
|
|
691
|
+
if (!m) continue;
|
|
692
|
+
out.push({
|
|
693
|
+
policyId: policy.id,
|
|
694
|
+
policyName: policy.name,
|
|
695
|
+
...policy.description !== void 0 ? { description: policy.description } : {},
|
|
696
|
+
severity: policy.severity,
|
|
697
|
+
onViolation: resolveOnViolation(policy),
|
|
698
|
+
ruleType: policy.rule.type,
|
|
699
|
+
match: m.match,
|
|
700
|
+
reason: m.reason
|
|
916
701
|
});
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
graph.on("nodeDropped", onNodeDropped);
|
|
920
|
-
graph.on("edgeAdded", onEdgeAdded);
|
|
921
|
-
graph.on("edgeDropped", onEdgeDropped);
|
|
922
|
-
graph.on("nodeAttributesUpdated", onNodeAttrsUpdated);
|
|
923
|
-
return () => {
|
|
924
|
-
graph.off("nodeAdded", onNodeAdded);
|
|
925
|
-
graph.off("nodeDropped", onNodeDropped);
|
|
926
|
-
graph.off("edgeAdded", onEdgeAdded);
|
|
927
|
-
graph.off("edgeDropped", onEdgeDropped);
|
|
928
|
-
graph.off("nodeAttributesUpdated", onNodeAttrsUpdated);
|
|
929
|
-
};
|
|
702
|
+
}
|
|
703
|
+
return out;
|
|
930
704
|
}
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
info: "log",
|
|
935
|
-
warning: "alert",
|
|
936
|
-
error: "alert",
|
|
937
|
-
critical: "block"
|
|
938
|
-
};
|
|
939
|
-
function resolveOnViolation(policy) {
|
|
940
|
-
return policy.onViolation ?? DEFAULT_ACTION_BY_SEVERITY[policy.severity];
|
|
705
|
+
function requiredProvenanceList(required) {
|
|
706
|
+
if (Array.isArray(required)) return required.join(" | ");
|
|
707
|
+
return String(required);
|
|
941
708
|
}
|
|
942
|
-
function
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
message,
|
|
952
|
-
observedAt: new Date(ctx.now()).toISOString()
|
|
953
|
-
};
|
|
709
|
+
function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
|
|
710
|
+
const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
|
|
711
|
+
for (const edgeId of incident) {
|
|
712
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
713
|
+
if (e.type !== edgeType) continue;
|
|
714
|
+
if (requiredOtherEnd === void 0) return true;
|
|
715
|
+
if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
|
|
716
|
+
}
|
|
717
|
+
return false;
|
|
954
718
|
}
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
if (
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
719
|
+
function blastRadiusSubjectReaching(graph, rule, nodeId) {
|
|
720
|
+
let found = null;
|
|
721
|
+
graph.forEachNode((subjId, attrs) => {
|
|
722
|
+
if (found !== null) return;
|
|
723
|
+
if (subjId === nodeId) return;
|
|
724
|
+
if (attrs.type !== rule.nodeType) return;
|
|
725
|
+
const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
|
|
726
|
+
if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
|
|
727
|
+
});
|
|
728
|
+
return found;
|
|
729
|
+
}
|
|
730
|
+
function matchPolicyToNode(graph, policy, nodeId, node) {
|
|
731
|
+
const rule = policy.rule;
|
|
732
|
+
switch (rule.type) {
|
|
733
|
+
case "structural": {
|
|
734
|
+
if (node.type === rule.fromNodeType) {
|
|
735
|
+
return {
|
|
736
|
+
match: "subject",
|
|
737
|
+
reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
if (node.type === rule.toNodeType) {
|
|
741
|
+
return {
|
|
742
|
+
match: "region",
|
|
743
|
+
reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
|
|
744
|
+
};
|
|
974
745
|
}
|
|
746
|
+
return null;
|
|
975
747
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
rule
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
ctx
|
|
985
|
-
)
|
|
986
|
-
);
|
|
748
|
+
case "ownership": {
|
|
749
|
+
if (node.type === rule.nodeType) {
|
|
750
|
+
return {
|
|
751
|
+
match: "subject",
|
|
752
|
+
reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
return null;
|
|
987
756
|
}
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
makeViolation(
|
|
1005
|
-
policy,
|
|
1006
|
-
rule,
|
|
1007
|
-
id,
|
|
1008
|
-
`${rule.nodeType} ${id} is missing required field "${rule.field}"`,
|
|
1009
|
-
{ nodeId: id },
|
|
1010
|
-
ctx
|
|
1011
|
-
)
|
|
1012
|
-
);
|
|
757
|
+
case "blast-radius": {
|
|
758
|
+
const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
|
|
759
|
+
if (node.type === rule.nodeType) {
|
|
760
|
+
return {
|
|
761
|
+
match: "subject",
|
|
762
|
+
reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
|
|
766
|
+
if (subject) {
|
|
767
|
+
return {
|
|
768
|
+
match: "region",
|
|
769
|
+
reason: `this node is in the blast radius of ${subject} (a ${rule.nodeType} held to a blast radius of ${rule.maxAffected}${depthLabel}) \u2014 it breaks if that ${rule.nodeType} changes`
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
return null;
|
|
1013
773
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
const requiredList = [...required].join(" | ");
|
|
1031
|
-
violations.push(
|
|
1032
|
-
makeViolation(
|
|
1033
|
-
policy,
|
|
1034
|
-
rule,
|
|
1035
|
-
edgeId,
|
|
1036
|
-
`${rule.edgeType} edge ${edgeId} has provenance ${e.provenance}; required ${requiredList}`,
|
|
1037
|
-
{ edgeId },
|
|
1038
|
-
ctx
|
|
1039
|
-
)
|
|
1040
|
-
);
|
|
1041
|
-
}
|
|
1042
|
-
});
|
|
1043
|
-
return violations;
|
|
1044
|
-
};
|
|
1045
|
-
var evaluateBlastRadius = ({
|
|
1046
|
-
graph,
|
|
1047
|
-
policy,
|
|
1048
|
-
rule,
|
|
1049
|
-
ctx
|
|
1050
|
-
}) => {
|
|
1051
|
-
const violations = [];
|
|
1052
|
-
const depth = rule.depth;
|
|
1053
|
-
graph.forEachNode((id, attrs) => {
|
|
1054
|
-
const a = attrs;
|
|
1055
|
-
if (a.type !== rule.nodeType) return;
|
|
1056
|
-
const result = depth !== void 0 ? getBlastRadius(graph, id, depth) : getBlastRadius(graph, id);
|
|
1057
|
-
if (result.totalAffected > rule.maxAffected) {
|
|
1058
|
-
violations.push(
|
|
1059
|
-
makeViolation(
|
|
1060
|
-
policy,
|
|
1061
|
-
rule,
|
|
1062
|
-
id,
|
|
1063
|
-
`${rule.nodeType} ${id} has blast radius ${result.totalAffected} > ${rule.maxAffected}`,
|
|
1064
|
-
{ nodeId: id, path: [id] },
|
|
1065
|
-
ctx
|
|
1066
|
-
)
|
|
1067
|
-
);
|
|
1068
|
-
}
|
|
1069
|
-
});
|
|
1070
|
-
return violations;
|
|
1071
|
-
};
|
|
1072
|
-
var evaluateCompatibility = ({
|
|
1073
|
-
graph,
|
|
1074
|
-
policy,
|
|
1075
|
-
rule,
|
|
1076
|
-
ctx
|
|
1077
|
-
}) => {
|
|
1078
|
-
const violations = [];
|
|
1079
|
-
const wantsKind = (kind) => rule.kind === void 0 || rule.kind === kind;
|
|
1080
|
-
graph.forEachNode((svcId, attrs) => {
|
|
1081
|
-
const a = attrs;
|
|
1082
|
-
if (a.type !== NodeType2.ServiceNode) return;
|
|
1083
|
-
const svc = a;
|
|
1084
|
-
const deps = svc.dependencies ?? {};
|
|
1085
|
-
if (wantsKind("driver-engine")) {
|
|
1086
|
-
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
1087
|
-
const e = graph.getEdgeAttributes(edgeId);
|
|
1088
|
-
if (e.type !== EdgeType2.CONNECTS_TO) continue;
|
|
1089
|
-
const dbAttrs = graph.getNodeAttributes(e.target);
|
|
1090
|
-
if (dbAttrs.type === NodeType2.FrontierNode) continue;
|
|
1091
|
-
if (dbAttrs.type !== NodeType2.DatabaseNode) continue;
|
|
1092
|
-
const db = dbAttrs;
|
|
1093
|
-
for (const pair of compatPairs()) {
|
|
1094
|
-
if (pair.engine !== db.engine) continue;
|
|
1095
|
-
const declared = deps[pair.driver];
|
|
1096
|
-
if (!declared) continue;
|
|
1097
|
-
const result = checkCompatibility(pair.driver, declared, db.engine, db.engineVersion);
|
|
1098
|
-
if (!result.compatible && result.reason) {
|
|
1099
|
-
violations.push(
|
|
1100
|
-
makeViolation(
|
|
1101
|
-
policy,
|
|
1102
|
-
rule,
|
|
1103
|
-
`${svcId}:driver-engine:${pair.driver}@${declared}:${db.engine}@${db.engineVersion}`,
|
|
1104
|
-
result.reason,
|
|
1105
|
-
{ nodeId: svcId, edgeId },
|
|
1106
|
-
ctx
|
|
1107
|
-
)
|
|
1108
|
-
);
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
if (wantsKind("node-engine")) {
|
|
1114
|
-
const serviceNodeRange = svc.nodeEngine;
|
|
1115
|
-
for (const constraint of nodeEngineConstraints()) {
|
|
1116
|
-
const declared = deps[constraint.package];
|
|
1117
|
-
if (!declared) continue;
|
|
1118
|
-
const result = checkNodeEngineConstraint(constraint, declared, serviceNodeRange);
|
|
1119
|
-
if (!result.compatible && result.reason) {
|
|
1120
|
-
violations.push(
|
|
1121
|
-
makeViolation(
|
|
1122
|
-
policy,
|
|
1123
|
-
rule,
|
|
1124
|
-
`${svcId}:node-engine:${constraint.package}@${declared}`,
|
|
1125
|
-
result.reason,
|
|
1126
|
-
{ nodeId: svcId },
|
|
1127
|
-
ctx
|
|
1128
|
-
)
|
|
1129
|
-
);
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
|
-
if (wantsKind("package-conflict")) {
|
|
1134
|
-
for (const conflict of packageConflicts()) {
|
|
1135
|
-
const declared = deps[conflict.package];
|
|
1136
|
-
if (!declared) continue;
|
|
1137
|
-
const requiredDeclared = deps[conflict.requires.name];
|
|
1138
|
-
const result = checkPackageConflict(conflict, declared, requiredDeclared);
|
|
1139
|
-
if (!result.compatible && result.reason) {
|
|
1140
|
-
violations.push(
|
|
1141
|
-
makeViolation(
|
|
1142
|
-
policy,
|
|
1143
|
-
rule,
|
|
1144
|
-
`${svcId}:package-conflict:${conflict.package}@${declared}`,
|
|
1145
|
-
result.reason,
|
|
1146
|
-
{ nodeId: svcId },
|
|
1147
|
-
ctx
|
|
1148
|
-
)
|
|
1149
|
-
);
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
if (wantsKind("deprecated-api")) {
|
|
1154
|
-
for (const dep of deprecatedApis()) {
|
|
1155
|
-
const declared = deps[dep.package];
|
|
1156
|
-
if (!declared) continue;
|
|
1157
|
-
const result = checkDeprecatedApi(dep, declared);
|
|
1158
|
-
if (!result.compatible && result.reason) {
|
|
1159
|
-
violations.push(
|
|
1160
|
-
makeViolation(
|
|
1161
|
-
policy,
|
|
1162
|
-
rule,
|
|
1163
|
-
`${svcId}:deprecated-api:${dep.package}@${declared}`,
|
|
1164
|
-
result.reason,
|
|
1165
|
-
{ nodeId: svcId },
|
|
1166
|
-
ctx
|
|
1167
|
-
)
|
|
1168
|
-
);
|
|
1169
|
-
}
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
});
|
|
1173
|
-
return violations;
|
|
1174
|
-
};
|
|
1175
|
-
var policyEvaluators = {
|
|
1176
|
-
structural: evaluateStructural,
|
|
1177
|
-
ownership: evaluateOwnership,
|
|
1178
|
-
provenance: evaluateProvenance,
|
|
1179
|
-
"blast-radius": evaluateBlastRadius,
|
|
1180
|
-
compatibility: evaluateCompatibility
|
|
1181
|
-
};
|
|
1182
|
-
function canPromoteFrontier(graph, frontierId2, policies, ctx) {
|
|
1183
|
-
if (policies.length === 0) return { allowed: true, violations: [] };
|
|
1184
|
-
const all = evaluateAllPolicies(graph, policies, ctx);
|
|
1185
|
-
const blocking = all.filter((v) => {
|
|
1186
|
-
if (v.onViolation !== "block") return false;
|
|
1187
|
-
return v.subject.nodeId === frontierId2 || v.subject.path?.includes(frontierId2) === true;
|
|
1188
|
-
});
|
|
1189
|
-
return { allowed: blocking.length === 0, violations: blocking };
|
|
1190
|
-
}
|
|
1191
|
-
function evaluateAllPolicies(graph, policies, ctx) {
|
|
1192
|
-
const out = [];
|
|
1193
|
-
for (const policy of policies) {
|
|
1194
|
-
const evaluator = policyEvaluators[policy.rule.type];
|
|
1195
|
-
const violations = evaluator({ graph, policy, rule: policy.rule, ctx });
|
|
1196
|
-
for (const v of violations) out.push(v);
|
|
1197
|
-
}
|
|
1198
|
-
return out;
|
|
1199
|
-
}
|
|
1200
|
-
function selectApplicablePolicies(graph, policies, nodeId) {
|
|
1201
|
-
if (!graph.hasNode(nodeId)) return [];
|
|
1202
|
-
const node = graph.getNodeAttributes(nodeId);
|
|
1203
|
-
const out = [];
|
|
1204
|
-
for (const policy of policies) {
|
|
1205
|
-
const m = matchPolicyToNode(graph, policy, nodeId, node);
|
|
1206
|
-
if (!m) continue;
|
|
1207
|
-
out.push({
|
|
1208
|
-
policyId: policy.id,
|
|
1209
|
-
policyName: policy.name,
|
|
1210
|
-
...policy.description !== void 0 ? { description: policy.description } : {},
|
|
1211
|
-
severity: policy.severity,
|
|
1212
|
-
onViolation: resolveOnViolation(policy),
|
|
1213
|
-
ruleType: policy.rule.type,
|
|
1214
|
-
match: m.match,
|
|
1215
|
-
reason: m.reason
|
|
1216
|
-
});
|
|
1217
|
-
}
|
|
1218
|
-
return out;
|
|
1219
|
-
}
|
|
1220
|
-
function requiredProvenanceList(required) {
|
|
1221
|
-
if (Array.isArray(required)) return required.join(" | ");
|
|
1222
|
-
return String(required);
|
|
1223
|
-
}
|
|
1224
|
-
function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
|
|
1225
|
-
const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
|
|
1226
|
-
for (const edgeId of incident) {
|
|
1227
|
-
const e = graph.getEdgeAttributes(edgeId);
|
|
1228
|
-
if (e.type !== edgeType) continue;
|
|
1229
|
-
if (requiredOtherEnd === void 0) return true;
|
|
1230
|
-
if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
|
|
1231
|
-
}
|
|
1232
|
-
return false;
|
|
1233
|
-
}
|
|
1234
|
-
function blastRadiusSubjectReaching(graph, rule, nodeId) {
|
|
1235
|
-
let found = null;
|
|
1236
|
-
graph.forEachNode((subjId, attrs) => {
|
|
1237
|
-
if (found !== null) return;
|
|
1238
|
-
if (subjId === nodeId) return;
|
|
1239
|
-
if (attrs.type !== rule.nodeType) return;
|
|
1240
|
-
const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
|
|
1241
|
-
if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
|
|
1242
|
-
});
|
|
1243
|
-
return found;
|
|
1244
|
-
}
|
|
1245
|
-
function matchPolicyToNode(graph, policy, nodeId, node) {
|
|
1246
|
-
const rule = policy.rule;
|
|
1247
|
-
switch (rule.type) {
|
|
1248
|
-
case "structural": {
|
|
1249
|
-
if (node.type === rule.fromNodeType) {
|
|
1250
|
-
return {
|
|
1251
|
-
match: "subject",
|
|
1252
|
-
reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
|
|
1253
|
-
};
|
|
1254
|
-
}
|
|
1255
|
-
if (node.type === rule.toNodeType) {
|
|
1256
|
-
return {
|
|
1257
|
-
match: "region",
|
|
1258
|
-
reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
|
|
1259
|
-
};
|
|
1260
|
-
}
|
|
1261
|
-
return null;
|
|
1262
|
-
}
|
|
1263
|
-
case "ownership": {
|
|
1264
|
-
if (node.type === rule.nodeType) {
|
|
1265
|
-
return {
|
|
1266
|
-
match: "subject",
|
|
1267
|
-
reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
|
|
1268
|
-
};
|
|
1269
|
-
}
|
|
1270
|
-
return null;
|
|
1271
|
-
}
|
|
1272
|
-
case "blast-radius": {
|
|
1273
|
-
const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
|
|
1274
|
-
if (node.type === rule.nodeType) {
|
|
1275
|
-
return {
|
|
1276
|
-
match: "subject",
|
|
1277
|
-
reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
|
|
1278
|
-
};
|
|
1279
|
-
}
|
|
1280
|
-
const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
|
|
1281
|
-
if (subject) {
|
|
1282
|
-
return {
|
|
1283
|
-
match: "region",
|
|
1284
|
-
reason: `this node is in the blast radius of ${subject} (a ${rule.nodeType} held to a blast radius of ${rule.maxAffected}${depthLabel}) \u2014 it breaks if that ${rule.nodeType} changes`
|
|
1285
|
-
};
|
|
1286
|
-
}
|
|
1287
|
-
return null;
|
|
1288
|
-
}
|
|
1289
|
-
case "compatibility": {
|
|
1290
|
-
const kindLabel = rule.kind ?? "all compat shapes";
|
|
1291
|
-
if (node.type === NodeType2.ServiceNode) {
|
|
1292
|
-
return {
|
|
1293
|
-
match: "subject",
|
|
1294
|
-
reason: `this service's dependencies are compatibility-checked (${kindLabel})`
|
|
1295
|
-
};
|
|
1296
|
-
}
|
|
1297
|
-
const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
|
|
1298
|
-
if (reachesDriverEngine && node.type === NodeType2.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, EdgeType2.CONNECTS_TO)) {
|
|
1299
|
-
return {
|
|
1300
|
-
match: "region",
|
|
1301
|
-
reason: "services connecting to this database have their driver/engine compatibility checked against it"
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
return null;
|
|
774
|
+
case "compatibility": {
|
|
775
|
+
const kindLabel = rule.kind ?? "all compat shapes";
|
|
776
|
+
if (node.type === NodeType.ServiceNode) {
|
|
777
|
+
return {
|
|
778
|
+
match: "subject",
|
|
779
|
+
reason: `this service's dependencies are compatibility-checked (${kindLabel})`
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
|
|
783
|
+
if (reachesDriverEngine && node.type === NodeType.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, EdgeType.CONNECTS_TO)) {
|
|
784
|
+
return {
|
|
785
|
+
match: "region",
|
|
786
|
+
reason: "services connecting to this database have their driver/engine compatibility checked against it"
|
|
787
|
+
};
|
|
788
|
+
}
|
|
789
|
+
return null;
|
|
1305
790
|
}
|
|
1306
791
|
case "provenance": {
|
|
1307
792
|
const requiredList = requiredProvenanceList(rule.required);
|
|
@@ -1372,11 +857,11 @@ var PolicyViolationsLog = class {
|
|
|
1372
857
|
|
|
1373
858
|
// src/ingest.ts
|
|
1374
859
|
import {
|
|
1375
|
-
EdgeType as
|
|
860
|
+
EdgeType as EdgeType2,
|
|
1376
861
|
GraphEdgeSchema,
|
|
1377
862
|
GraphNodeSchema,
|
|
1378
|
-
NodeType as
|
|
1379
|
-
Provenance
|
|
863
|
+
NodeType as NodeType2,
|
|
864
|
+
Provenance,
|
|
1380
865
|
confidenceForObservedSignal,
|
|
1381
866
|
databaseId,
|
|
1382
867
|
localDatabaseId,
|
|
@@ -1560,9 +1045,30 @@ function isLoopbackHost(host) {
|
|
|
1560
1045
|
const h = host.toLowerCase();
|
|
1561
1046
|
return h === "localhost" || h === "ip6-localhost" || h === "::1" || h === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(h);
|
|
1562
1047
|
}
|
|
1048
|
+
var CODE_FILE_PATH_ATTR = "code.file.path";
|
|
1563
1049
|
var CODE_FILEPATH_ATTR = "code.filepath";
|
|
1050
|
+
var CODE_LINE_NUMBER_ATTR = "code.line.number";
|
|
1564
1051
|
var CODE_LINENO_ATTR = "code.lineno";
|
|
1052
|
+
var CODE_FUNCTION_NAME_ATTR = "code.function.name";
|
|
1565
1053
|
var CODE_FUNCTION_ATTR = "code.function";
|
|
1054
|
+
function codeFilepathOf(attrs) {
|
|
1055
|
+
const stable = attrs[CODE_FILE_PATH_ATTR];
|
|
1056
|
+
if (typeof stable === "string" && stable.length > 0) return stable;
|
|
1057
|
+
const prior = attrs[CODE_FILEPATH_ATTR];
|
|
1058
|
+
return typeof prior === "string" && prior.length > 0 ? prior : void 0;
|
|
1059
|
+
}
|
|
1060
|
+
function codeLinenoOf(attrs) {
|
|
1061
|
+
const stable = attrs[CODE_LINE_NUMBER_ATTR];
|
|
1062
|
+
if (typeof stable === "number" && Number.isFinite(stable)) return stable;
|
|
1063
|
+
const prior = attrs[CODE_LINENO_ATTR];
|
|
1064
|
+
return typeof prior === "number" && Number.isFinite(prior) ? prior : void 0;
|
|
1065
|
+
}
|
|
1066
|
+
function codeFunctionOf(attrs) {
|
|
1067
|
+
const stable = attrs[CODE_FUNCTION_NAME_ATTR];
|
|
1068
|
+
if (typeof stable === "string" && stable.length > 0) return stable;
|
|
1069
|
+
const prior = attrs[CODE_FUNCTION_ATTR];
|
|
1070
|
+
return typeof prior === "string" && prior.length > 0 ? prior : void 0;
|
|
1071
|
+
}
|
|
1566
1072
|
function toPosix(p) {
|
|
1567
1073
|
return p.split("\\").join("/");
|
|
1568
1074
|
}
|
|
@@ -1640,10 +1146,9 @@ function resolveDistToSrc(absFilepath, line) {
|
|
|
1640
1146
|
}
|
|
1641
1147
|
}
|
|
1642
1148
|
function callSiteFromSpan(span, serviceNode, scanPath) {
|
|
1643
|
-
const filepath = span.attributes
|
|
1644
|
-
if (
|
|
1645
|
-
|
|
1646
|
-
let line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
|
|
1149
|
+
const filepath = codeFilepathOf(span.attributes);
|
|
1150
|
+
if (filepath === void 0) return null;
|
|
1151
|
+
let line = codeLinenoOf(span.attributes);
|
|
1647
1152
|
const abs = toPosix(filepath).replace(/^file:\/\//, "");
|
|
1648
1153
|
const resolved = resolveDistToSrc(abs, line);
|
|
1649
1154
|
let effectivePath = filepath;
|
|
@@ -1658,8 +1163,7 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
|
|
|
1658
1163
|
if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && serviceNode?.name) {
|
|
1659
1164
|
warnNoSourceMaps(serviceNode.name);
|
|
1660
1165
|
}
|
|
1661
|
-
const
|
|
1662
|
-
const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
|
|
1166
|
+
const fn = codeFunctionOf(span.attributes);
|
|
1663
1167
|
return {
|
|
1664
1168
|
relPath,
|
|
1665
1169
|
...line !== void 0 ? { line } : {},
|
|
@@ -1672,7 +1176,7 @@ function reconcileObservedRelPath(graph, serviceName, relPath) {
|
|
|
1672
1176
|
let best = null;
|
|
1673
1177
|
graph.forEachNode((_id, attrs) => {
|
|
1674
1178
|
const a = attrs;
|
|
1675
|
-
if (a.type !==
|
|
1179
|
+
if (a.type !== NodeType2.FileNode || a.service !== serviceName) return;
|
|
1676
1180
|
if (a.discoveredVia === "otel") return;
|
|
1677
1181
|
const p = a.path;
|
|
1678
1182
|
if (!p) return;
|
|
@@ -1689,7 +1193,7 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
|
1689
1193
|
const language = languageForExt(relPath);
|
|
1690
1194
|
const node = {
|
|
1691
1195
|
id: fileNodeId,
|
|
1692
|
-
type:
|
|
1196
|
+
type: NodeType2.FileNode,
|
|
1693
1197
|
service: serviceName,
|
|
1694
1198
|
path: relPath,
|
|
1695
1199
|
...language ? { language } : {},
|
|
@@ -1698,14 +1202,14 @@ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
|
1698
1202
|
};
|
|
1699
1203
|
graph.addNode(fileNodeId, node);
|
|
1700
1204
|
}
|
|
1701
|
-
const containsId = makeObservedEdgeId(
|
|
1205
|
+
const containsId = makeObservedEdgeId(EdgeType2.CONTAINS, serviceNodeId, fileNodeId);
|
|
1702
1206
|
if (!graph.hasEdge(containsId)) {
|
|
1703
1207
|
const edge = {
|
|
1704
1208
|
id: containsId,
|
|
1705
1209
|
source: serviceNodeId,
|
|
1706
1210
|
target: fileNodeId,
|
|
1707
|
-
type:
|
|
1708
|
-
provenance:
|
|
1211
|
+
type: EdgeType2.CONTAINS,
|
|
1212
|
+
provenance: Provenance.OBSERVED
|
|
1709
1213
|
};
|
|
1710
1214
|
graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
|
|
1711
1215
|
}
|
|
@@ -1720,9 +1224,9 @@ function makeInferredEdgeId(type, source, target) {
|
|
|
1720
1224
|
var INFERRED_CONFIDENCE = 0.6;
|
|
1721
1225
|
var STITCH_MAX_DEPTH = 2;
|
|
1722
1226
|
var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1227
|
+
EdgeType2.CALLS,
|
|
1228
|
+
EdgeType2.CONNECTS_TO,
|
|
1229
|
+
EdgeType2.DEPENDS_ON
|
|
1726
1230
|
]);
|
|
1727
1231
|
var WIRE_SPAN_KIND_CLIENT = 3;
|
|
1728
1232
|
var WIRE_SPAN_KIND_PRODUCER = 4;
|
|
@@ -1742,7 +1246,7 @@ function ensureGraphqlOperationNode(graph, serviceName, operationType, operation
|
|
|
1742
1246
|
if (graph.hasNode(id)) return id;
|
|
1743
1247
|
const node = {
|
|
1744
1248
|
id,
|
|
1745
|
-
type:
|
|
1249
|
+
type: NodeType2.GraphQLOperationNode,
|
|
1746
1250
|
name: operationName,
|
|
1747
1251
|
service: serviceName,
|
|
1748
1252
|
operationType: operationType.toLowerCase(),
|
|
@@ -1760,7 +1264,7 @@ function ensureGrpcMethodNode(graph, rpcService, rpcMethod) {
|
|
|
1760
1264
|
if (graph.hasNode(id)) return id;
|
|
1761
1265
|
const node = {
|
|
1762
1266
|
id,
|
|
1763
|
-
type:
|
|
1267
|
+
type: NodeType2.GrpcMethodNode,
|
|
1764
1268
|
name: `${rpcService}/${rpcMethod}`,
|
|
1765
1269
|
rpcService,
|
|
1766
1270
|
rpcMethod,
|
|
@@ -1777,7 +1281,7 @@ function ensureWebsocketChannelNode(graph, serviceName, channel) {
|
|
|
1777
1281
|
if (graph.hasNode(id)) return id;
|
|
1778
1282
|
const node = {
|
|
1779
1283
|
id,
|
|
1780
|
-
type:
|
|
1284
|
+
type: NodeType2.WebSocketChannelNode,
|
|
1781
1285
|
name: channel,
|
|
1782
1286
|
service: serviceName,
|
|
1783
1287
|
channel,
|
|
@@ -1794,7 +1298,7 @@ function ensureMessagingDestinationNode(graph, system, destination) {
|
|
|
1794
1298
|
if (graph.hasNode(id)) return id;
|
|
1795
1299
|
const node = {
|
|
1796
1300
|
id,
|
|
1797
|
-
type:
|
|
1301
|
+
type: NodeType2.InfraNode,
|
|
1798
1302
|
name: destination,
|
|
1799
1303
|
provider: "self",
|
|
1800
1304
|
kind: messagingDestinationKind(system)
|
|
@@ -1848,7 +1352,7 @@ function resolveServiceId(graph, host, env) {
|
|
|
1848
1352
|
graph.forEachNode((id, attrs) => {
|
|
1849
1353
|
if (sameEnv) return;
|
|
1850
1354
|
const a = attrs;
|
|
1851
|
-
if (a.type !==
|
|
1355
|
+
if (a.type !== NodeType2.ServiceNode) return;
|
|
1852
1356
|
const matchesByName = a.name === host;
|
|
1853
1357
|
const matchesByAlias = a.aliases ? a.aliases.includes(host) : false;
|
|
1854
1358
|
if (!matchesByName && !matchesByAlias) return;
|
|
@@ -1870,7 +1374,7 @@ function ensureServiceNode(graph, serviceName, env) {
|
|
|
1870
1374
|
if (graph.hasNode(id)) return id;
|
|
1871
1375
|
const node = {
|
|
1872
1376
|
id,
|
|
1873
|
-
type:
|
|
1377
|
+
type: NodeType2.ServiceNode,
|
|
1874
1378
|
name: serviceName,
|
|
1875
1379
|
language: "unknown",
|
|
1876
1380
|
discoveredVia: "otel",
|
|
@@ -1884,7 +1388,7 @@ function ensureInfraNode(graph, kind, name, provider) {
|
|
|
1884
1388
|
if (graph.hasNode(id)) return id;
|
|
1885
1389
|
const node = {
|
|
1886
1390
|
id,
|
|
1887
|
-
type:
|
|
1391
|
+
type: NodeType2.InfraNode,
|
|
1888
1392
|
name,
|
|
1889
1393
|
provider,
|
|
1890
1394
|
kind
|
|
@@ -1897,7 +1401,7 @@ function ensureDatabaseNode(graph, host, engine) {
|
|
|
1897
1401
|
if (graph.hasNode(id)) return id;
|
|
1898
1402
|
const node = {
|
|
1899
1403
|
id,
|
|
1900
|
-
type:
|
|
1404
|
+
type: NodeType2.DatabaseNode,
|
|
1901
1405
|
name: host,
|
|
1902
1406
|
engine,
|
|
1903
1407
|
engineVersion: "unknown",
|
|
@@ -1913,7 +1417,7 @@ function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
|
|
|
1913
1417
|
if (graph.hasNode(id)) return id;
|
|
1914
1418
|
const node = {
|
|
1915
1419
|
id,
|
|
1916
|
-
type:
|
|
1420
|
+
type: NodeType2.DatabaseNode,
|
|
1917
1421
|
name,
|
|
1918
1422
|
engine,
|
|
1919
1423
|
engineVersion: "unknown",
|
|
@@ -1928,17 +1432,17 @@ function findDeclaredDatabaseForService(graph, serviceNodeId, engine) {
|
|
|
1928
1432
|
const sources = [serviceNodeId];
|
|
1929
1433
|
for (const edgeId of graph.outboundEdges(serviceNodeId)) {
|
|
1930
1434
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1931
|
-
if (e.type ===
|
|
1435
|
+
if (e.type === EdgeType2.CONTAINS) sources.push(e.target);
|
|
1932
1436
|
}
|
|
1933
1437
|
const matches = /* @__PURE__ */ new Set();
|
|
1934
1438
|
for (const src of sources) {
|
|
1935
1439
|
if (!graph.hasNode(src)) continue;
|
|
1936
1440
|
for (const edgeId of graph.outboundEdges(src)) {
|
|
1937
1441
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
1938
|
-
if (edge.type !==
|
|
1442
|
+
if (edge.type !== EdgeType2.CONNECTS_TO || edge.provenance !== Provenance.EXTRACTED) continue;
|
|
1939
1443
|
if (!graph.hasNode(edge.target)) continue;
|
|
1940
1444
|
const target = graph.getNodeAttributes(edge.target);
|
|
1941
|
-
if (target.type !==
|
|
1445
|
+
if (target.type !== NodeType2.DatabaseNode || target.engine !== engine) continue;
|
|
1942
1446
|
matches.add(edge.target);
|
|
1943
1447
|
}
|
|
1944
1448
|
}
|
|
@@ -1953,7 +1457,7 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
1953
1457
|
}
|
|
1954
1458
|
const node = {
|
|
1955
1459
|
id,
|
|
1956
|
-
type:
|
|
1460
|
+
type: NodeType2.FrontierNode,
|
|
1957
1461
|
name: host,
|
|
1958
1462
|
host,
|
|
1959
1463
|
firstObserved: ts,
|
|
@@ -1977,7 +1481,7 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
1977
1481
|
};
|
|
1978
1482
|
const updated = {
|
|
1979
1483
|
...existing,
|
|
1980
|
-
provenance:
|
|
1484
|
+
provenance: Provenance.OBSERVED,
|
|
1981
1485
|
lastObserved: ts,
|
|
1982
1486
|
callCount: newSpanCount,
|
|
1983
1487
|
signal: newSignal,
|
|
@@ -1998,7 +1502,7 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
|
|
|
1998
1502
|
source,
|
|
1999
1503
|
target,
|
|
2000
1504
|
type,
|
|
2001
|
-
provenance:
|
|
1505
|
+
provenance: Provenance.OBSERVED,
|
|
2002
1506
|
confidence: confidenceForObservedSignal(signal),
|
|
2003
1507
|
lastObserved: ts,
|
|
2004
1508
|
callCount: 1,
|
|
@@ -2021,7 +1525,7 @@ function stitchTrace(graph, sourceServiceId, ts) {
|
|
|
2021
1525
|
const outbound = graph.outboundEdges(nodeId);
|
|
2022
1526
|
for (const edgeId of outbound) {
|
|
2023
1527
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
2024
|
-
if (edge.provenance !==
|
|
1528
|
+
if (edge.provenance !== Provenance.EXTRACTED) continue;
|
|
2025
1529
|
if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
|
|
2026
1530
|
if (graph.hasEdge(observedEdgeId(edge.source, edge.target, edge.type))) continue;
|
|
2027
1531
|
upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
|
|
@@ -2045,7 +1549,7 @@ function upsertInferredEdge(graph, type, source, target, ts) {
|
|
|
2045
1549
|
source,
|
|
2046
1550
|
target,
|
|
2047
1551
|
type,
|
|
2048
|
-
provenance:
|
|
1552
|
+
provenance: Provenance.INFERRED,
|
|
2049
1553
|
confidence: INFERRED_CONFIDENCE,
|
|
2050
1554
|
lastObserved: ts
|
|
2051
1555
|
};
|
|
@@ -2221,7 +1725,7 @@ async function handleSpan(ctx, span) {
|
|
|
2221
1725
|
}
|
|
2222
1726
|
const result = upsertObservedEdge(
|
|
2223
1727
|
ctx.graph,
|
|
2224
|
-
|
|
1728
|
+
EdgeType2.CONNECTS_TO,
|
|
2225
1729
|
observedSource(),
|
|
2226
1730
|
targetId,
|
|
2227
1731
|
ts,
|
|
@@ -2233,7 +1737,7 @@ async function handleSpan(ctx, span) {
|
|
|
2233
1737
|
const collectionId = ensureInfraNode(ctx.graph, "mongodb-collection", span.dbCollection, "self");
|
|
2234
1738
|
upsertObservedEdge(
|
|
2235
1739
|
ctx.graph,
|
|
2236
|
-
|
|
1740
|
+
EdgeType2.CALLS,
|
|
2237
1741
|
observedSource(),
|
|
2238
1742
|
collectionId,
|
|
2239
1743
|
ts,
|
|
@@ -2241,6 +1745,18 @@ async function handleSpan(ctx, span) {
|
|
|
2241
1745
|
callSiteEvidence
|
|
2242
1746
|
);
|
|
2243
1747
|
}
|
|
1748
|
+
if (span.dbTable) {
|
|
1749
|
+
const tableId = ensureInfraNode(ctx.graph, "sql-table", span.dbTable, "self");
|
|
1750
|
+
upsertObservedEdge(
|
|
1751
|
+
ctx.graph,
|
|
1752
|
+
EdgeType2.CALLS,
|
|
1753
|
+
observedSource(),
|
|
1754
|
+
tableId,
|
|
1755
|
+
ts,
|
|
1756
|
+
isError,
|
|
1757
|
+
callSiteEvidence
|
|
1758
|
+
);
|
|
1759
|
+
}
|
|
2244
1760
|
}
|
|
2245
1761
|
} else if (span.messagingSystem && span.messagingDestination && spanMintsMessagingEdge(span.kind)) {
|
|
2246
1762
|
const targetId = ensureMessagingDestinationNode(
|
|
@@ -2248,7 +1764,7 @@ async function handleSpan(ctx, span) {
|
|
|
2248
1764
|
span.messagingSystem,
|
|
2249
1765
|
span.messagingDestination
|
|
2250
1766
|
);
|
|
2251
|
-
const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ?
|
|
1767
|
+
const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? EdgeType2.CONSUMES_FROM : EdgeType2.PUBLISHES_TO;
|
|
2252
1768
|
const result = upsertObservedEdge(
|
|
2253
1769
|
ctx.graph,
|
|
2254
1770
|
edgeType,
|
|
@@ -2268,7 +1784,7 @@ async function handleSpan(ctx, span) {
|
|
|
2268
1784
|
);
|
|
2269
1785
|
const result = upsertObservedEdge(
|
|
2270
1786
|
ctx.graph,
|
|
2271
|
-
|
|
1787
|
+
EdgeType2.CONTAINS,
|
|
2272
1788
|
observedSource(),
|
|
2273
1789
|
targetId,
|
|
2274
1790
|
ts,
|
|
@@ -2280,7 +1796,7 @@ async function handleSpan(ctx, span) {
|
|
|
2280
1796
|
const targetId = ensureGrpcMethodNode(ctx.graph, span.rpcService, span.rpcMethod);
|
|
2281
1797
|
const result = upsertObservedEdge(
|
|
2282
1798
|
ctx.graph,
|
|
2283
|
-
|
|
1799
|
+
EdgeType2.CONTAINS,
|
|
2284
1800
|
observedSource(),
|
|
2285
1801
|
targetId,
|
|
2286
1802
|
ts,
|
|
@@ -2296,7 +1812,7 @@ async function handleSpan(ctx, span) {
|
|
|
2296
1812
|
);
|
|
2297
1813
|
const result = upsertObservedEdge(
|
|
2298
1814
|
ctx.graph,
|
|
2299
|
-
|
|
1815
|
+
EdgeType2.CONNECTS_TO,
|
|
2300
1816
|
observedSource(),
|
|
2301
1817
|
targetId,
|
|
2302
1818
|
ts,
|
|
@@ -2312,7 +1828,7 @@ async function handleSpan(ctx, span) {
|
|
|
2312
1828
|
if (targetId && targetId !== sourceId) {
|
|
2313
1829
|
upsertObservedEdge(
|
|
2314
1830
|
ctx.graph,
|
|
2315
|
-
|
|
1831
|
+
EdgeType2.CALLS,
|
|
2316
1832
|
observedSource(),
|
|
2317
1833
|
targetId,
|
|
2318
1834
|
ts,
|
|
@@ -2325,7 +1841,7 @@ async function handleSpan(ctx, span) {
|
|
|
2325
1841
|
const frontierNodeId = ensureFrontierNode(ctx.graph, host, ts);
|
|
2326
1842
|
upsertObservedEdge(
|
|
2327
1843
|
ctx.graph,
|
|
2328
|
-
|
|
1844
|
+
EdgeType2.CALLS,
|
|
2329
1845
|
observedSource(),
|
|
2330
1846
|
frontierNodeId,
|
|
2331
1847
|
ts,
|
|
@@ -2336,311 +1852,826 @@ async function handleSpan(ctx, span) {
|
|
|
2336
1852
|
resolvedViaAddress = true;
|
|
2337
1853
|
}
|
|
2338
1854
|
}
|
|
2339
|
-
if (!resolvedViaAddress && span.parentSpanId) {
|
|
2340
|
-
const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
|
|
2341
|
-
if (parent && parent.service !== span.service) {
|
|
2342
|
-
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
2343
|
-
const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
|
|
2344
|
-
const fallbackEvidence = parent.callSite ? {
|
|
2345
|
-
file: reconcileObservedRelPath(
|
|
2346
|
-
ctx.graph,
|
|
2347
|
-
parent.service,
|
|
2348
|
-
parent.callSite.relPath
|
|
2349
|
-
),
|
|
2350
|
-
...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
|
|
2351
|
-
} : void 0;
|
|
2352
|
-
upsertObservedEdge(
|
|
2353
|
-
ctx.graph,
|
|
2354
|
-
|
|
2355
|
-
fallbackSource,
|
|
2356
|
-
sourceId,
|
|
2357
|
-
ts,
|
|
2358
|
-
isError,
|
|
2359
|
-
fallbackEvidence
|
|
2360
|
-
);
|
|
2361
|
-
}
|
|
1855
|
+
if (!resolvedViaAddress && span.parentSpanId) {
|
|
1856
|
+
const parent = lookupParentSpan(span.traceId, span.parentSpanId, nowMs);
|
|
1857
|
+
if (parent && parent.service !== span.service) {
|
|
1858
|
+
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
1859
|
+
const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
|
|
1860
|
+
const fallbackEvidence = parent.callSite ? {
|
|
1861
|
+
file: reconcileObservedRelPath(
|
|
1862
|
+
ctx.graph,
|
|
1863
|
+
parent.service,
|
|
1864
|
+
parent.callSite.relPath
|
|
1865
|
+
),
|
|
1866
|
+
...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
|
|
1867
|
+
} : void 0;
|
|
1868
|
+
upsertObservedEdge(
|
|
1869
|
+
ctx.graph,
|
|
1870
|
+
EdgeType2.CALLS,
|
|
1871
|
+
fallbackSource,
|
|
1872
|
+
sourceId,
|
|
1873
|
+
ts,
|
|
1874
|
+
isError,
|
|
1875
|
+
fallbackEvidence
|
|
1876
|
+
);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
if (span.statusCode === 2) {
|
|
1881
|
+
stitchTrace(ctx.graph, sourceId, ts);
|
|
1882
|
+
if (ctx.writeErrorEventInline !== false) {
|
|
1883
|
+
const attrs = sanitizeAttributes(span.attributes);
|
|
1884
|
+
const ev = {
|
|
1885
|
+
id: `${span.traceId}:${span.spanId}`,
|
|
1886
|
+
timestamp: ts,
|
|
1887
|
+
service: span.service,
|
|
1888
|
+
traceId: span.traceId,
|
|
1889
|
+
spanId: span.spanId,
|
|
1890
|
+
errorMessage: incidentMessage(span),
|
|
1891
|
+
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1892
|
+
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1893
|
+
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
1894
|
+
affectedNode
|
|
1895
|
+
};
|
|
1896
|
+
await appendErrorEvent(ctx, ev);
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
if (span.statusCode !== 2) {
|
|
1900
|
+
const status = httpResponseStatus(span);
|
|
1901
|
+
if (span.exception) {
|
|
1902
|
+
await recordExceptionIncident(ctx, span, ts);
|
|
1903
|
+
} else if (status !== void 0 && status >= 500) {
|
|
1904
|
+
await recordFailingResponseIncident(ctx, span, sourceId, ts, status, 1);
|
|
1905
|
+
} else if (status !== void 0 && status >= 400 && spanMintsObservedEdge(span.kind)) {
|
|
1906
|
+
await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status);
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
void affectedNode;
|
|
1910
|
+
if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
|
|
1911
|
+
}
|
|
1912
|
+
function promoteFrontierNodes(graph, opts = {}) {
|
|
1913
|
+
const aliasIndex = /* @__PURE__ */ new Map();
|
|
1914
|
+
graph.forEachNode((id, attrs) => {
|
|
1915
|
+
const a = attrs;
|
|
1916
|
+
if (a.type !== NodeType2.ServiceNode) return;
|
|
1917
|
+
aliasIndex.set(a.name, id);
|
|
1918
|
+
if (a.aliases) {
|
|
1919
|
+
for (const alias of a.aliases) aliasIndex.set(alias, id);
|
|
1920
|
+
}
|
|
1921
|
+
});
|
|
1922
|
+
const toPromote = [];
|
|
1923
|
+
graph.forEachNode((id, attrs) => {
|
|
1924
|
+
const a = attrs;
|
|
1925
|
+
if (a.type !== NodeType2.FrontierNode) return;
|
|
1926
|
+
const target = aliasIndex.get(a.host);
|
|
1927
|
+
if (!target) return;
|
|
1928
|
+
if (target === id) return;
|
|
1929
|
+
toPromote.push({ frontierId: id, serviceId: target });
|
|
1930
|
+
});
|
|
1931
|
+
let promoted = 0;
|
|
1932
|
+
for (const { frontierId: frontierId2, serviceId: serviceId5 } of toPromote) {
|
|
1933
|
+
if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
|
|
1934
|
+
const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
|
|
1935
|
+
if (!gate.allowed) {
|
|
1936
|
+
continue;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
rewireFrontierEdges(graph, frontierId2, serviceId5);
|
|
1940
|
+
graph.dropNode(frontierId2);
|
|
1941
|
+
promoted++;
|
|
1942
|
+
}
|
|
1943
|
+
return promoted;
|
|
1944
|
+
}
|
|
1945
|
+
function rewireFrontierEdges(graph, frontierId2, serviceId5) {
|
|
1946
|
+
const inbound = [...graph.inboundEdges(frontierId2)];
|
|
1947
|
+
const outbound = [...graph.outboundEdges(frontierId2)];
|
|
1948
|
+
for (const edgeId of inbound) {
|
|
1949
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
1950
|
+
rebuildEdge(graph, edge, edge.source, serviceId5, edgeId);
|
|
1951
|
+
}
|
|
1952
|
+
for (const edgeId of outbound) {
|
|
1953
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
1954
|
+
rebuildEdge(graph, edge, serviceId5, edge.target, edgeId);
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
1958
|
+
graph.dropEdge(oldEdgeId);
|
|
1959
|
+
const newId = edge.provenance === Provenance.OBSERVED ? observedEdgeId(newSource, newTarget, edge.type) : edge.provenance === Provenance.INFERRED ? inferredEdgeId(newSource, newTarget, edge.type) : extractedEdgeId(newSource, newTarget, edge.type);
|
|
1960
|
+
if (graph.hasEdge(newId)) {
|
|
1961
|
+
const existing = graph.getEdgeAttributes(newId);
|
|
1962
|
+
const merged = {
|
|
1963
|
+
...existing,
|
|
1964
|
+
callCount: (existing.callCount ?? 0) + (edge.callCount ?? 0),
|
|
1965
|
+
lastObserved: pickLater(existing.lastObserved, edge.lastObserved)
|
|
1966
|
+
};
|
|
1967
|
+
graph.replaceEdgeAttributes(newId, merged);
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
const rebuilt = {
|
|
1971
|
+
...edge,
|
|
1972
|
+
id: newId,
|
|
1973
|
+
source: newSource,
|
|
1974
|
+
target: newTarget
|
|
1975
|
+
};
|
|
1976
|
+
graph.addEdgeWithKey(newId, newSource, newTarget, rebuilt);
|
|
1977
|
+
}
|
|
1978
|
+
function pickLater(a, b) {
|
|
1979
|
+
if (!a) return b;
|
|
1980
|
+
if (!b) return a;
|
|
1981
|
+
return new Date(a).getTime() >= new Date(b).getTime() ? a : b;
|
|
1982
|
+
}
|
|
1983
|
+
function makeSpanHandler(ctx) {
|
|
1984
|
+
return (span) => handleSpan(ctx, span);
|
|
1985
|
+
}
|
|
1986
|
+
async function markStaleEdges(graph, options = {}) {
|
|
1987
|
+
const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
|
|
1988
|
+
const now = options.now ?? Date.now();
|
|
1989
|
+
const events = [];
|
|
1990
|
+
const project = options.project ?? DEFAULT_PROJECT;
|
|
1991
|
+
graph.forEachEdge((id, attrs) => {
|
|
1992
|
+
const e = attrs;
|
|
1993
|
+
if (e.provenance !== Provenance.OBSERVED) return;
|
|
1994
|
+
if (!e.lastObserved) return;
|
|
1995
|
+
const threshold = thresholdForEdgeType(e.type, thresholds);
|
|
1996
|
+
const age = now - new Date(e.lastObserved).getTime();
|
|
1997
|
+
if (age > threshold) {
|
|
1998
|
+
const updated = { ...e, provenance: Provenance.STALE, confidence: 0.3 };
|
|
1999
|
+
graph.replaceEdgeAttributes(id, updated);
|
|
2000
|
+
events.push({
|
|
2001
|
+
edgeId: id,
|
|
2002
|
+
source: e.source,
|
|
2003
|
+
target: e.target,
|
|
2004
|
+
edgeType: e.type,
|
|
2005
|
+
thresholdMs: threshold,
|
|
2006
|
+
ageMs: age,
|
|
2007
|
+
lastObserved: e.lastObserved,
|
|
2008
|
+
transitionedAt: new Date(now).toISOString()
|
|
2009
|
+
});
|
|
2010
|
+
emitNeatEvent({
|
|
2011
|
+
type: "stale-transition",
|
|
2012
|
+
project,
|
|
2013
|
+
payload: {
|
|
2014
|
+
edgeId: id,
|
|
2015
|
+
from: Provenance.OBSERVED,
|
|
2016
|
+
to: Provenance.STALE
|
|
2017
|
+
}
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
if (options.staleEventsPath && events.length > 0) {
|
|
2022
|
+
await appendStaleEvents(options.staleEventsPath, events);
|
|
2023
|
+
}
|
|
2024
|
+
return { count: events.length, events };
|
|
2025
|
+
}
|
|
2026
|
+
async function appendStaleEvents(staleEventsPath, events) {
|
|
2027
|
+
await fs3.mkdir(path3.dirname(staleEventsPath), { recursive: true });
|
|
2028
|
+
const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2029
|
+
await fs3.appendFile(staleEventsPath, lines, "utf8");
|
|
2030
|
+
}
|
|
2031
|
+
async function readStaleEvents(staleEventsPath) {
|
|
2032
|
+
try {
|
|
2033
|
+
const raw = await fs3.readFile(staleEventsPath, "utf8");
|
|
2034
|
+
return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
2035
|
+
} catch (err) {
|
|
2036
|
+
if (err.code === "ENOENT") return [];
|
|
2037
|
+
throw err;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
function startStalenessLoop(graph, options = {}) {
|
|
2041
|
+
let stopped = false;
|
|
2042
|
+
const intervalMs = options.intervalMs ?? 6e4;
|
|
2043
|
+
const tick = () => {
|
|
2044
|
+
if (stopped) return;
|
|
2045
|
+
void (async () => {
|
|
2046
|
+
try {
|
|
2047
|
+
await markStaleEdges(graph, {
|
|
2048
|
+
thresholds: options.thresholds,
|
|
2049
|
+
staleEventsPath: options.staleEventsPath,
|
|
2050
|
+
project: options.project
|
|
2051
|
+
});
|
|
2052
|
+
if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
|
|
2053
|
+
} catch (err) {
|
|
2054
|
+
console.error("staleness tick failed", err);
|
|
2055
|
+
}
|
|
2056
|
+
})();
|
|
2057
|
+
};
|
|
2058
|
+
const interval = setInterval(tick, intervalMs);
|
|
2059
|
+
if (typeof interval.unref === "function") interval.unref();
|
|
2060
|
+
return () => {
|
|
2061
|
+
stopped = true;
|
|
2062
|
+
clearInterval(interval);
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
async function readErrorEvents(errorsPath) {
|
|
2066
|
+
try {
|
|
2067
|
+
const raw = await fs3.readFile(errorsPath, "utf8");
|
|
2068
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
2069
|
+
return dedupeIncidents(events);
|
|
2070
|
+
} catch (err) {
|
|
2071
|
+
if (err.code === "ENOENT") return [];
|
|
2072
|
+
throw err;
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
function isSynthesizedHttpIncident(ev) {
|
|
2076
|
+
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
2077
|
+
if (ev.errorType) return false;
|
|
2078
|
+
if (!ev.attributes) return false;
|
|
2079
|
+
const synth = httpFailureMessageFromAttrs(ev.attributes);
|
|
2080
|
+
return synth !== void 0 && synth === ev.errorMessage;
|
|
2081
|
+
}
|
|
2082
|
+
function dedupeIncidents(events) {
|
|
2083
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2084
|
+
const once = [];
|
|
2085
|
+
for (const ev of events) {
|
|
2086
|
+
const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
|
|
2087
|
+
if (key === void 0) {
|
|
2088
|
+
once.push(ev);
|
|
2089
|
+
continue;
|
|
2090
|
+
}
|
|
2091
|
+
if (seen.has(key)) continue;
|
|
2092
|
+
seen.add(key);
|
|
2093
|
+
once.push(ev);
|
|
2094
|
+
}
|
|
2095
|
+
const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
|
|
2096
|
+
const hasRealFailure = /* @__PURE__ */ new Set();
|
|
2097
|
+
for (const ev of once) {
|
|
2098
|
+
if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
|
|
2099
|
+
}
|
|
2100
|
+
return once.filter((ev) => {
|
|
2101
|
+
if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
|
|
2102
|
+
return !hasRealFailure.has(groupKey(ev));
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
2105
|
+
var SnapshotValidationError = class extends Error {
|
|
2106
|
+
constructor(issues) {
|
|
2107
|
+
super(`snapshot failed validation (${issues.length} invalid ${issues.length === 1 ? "entry" : "entries"})`);
|
|
2108
|
+
this.issues = issues;
|
|
2109
|
+
this.name = "SnapshotValidationError";
|
|
2110
|
+
}
|
|
2111
|
+
issues;
|
|
2112
|
+
};
|
|
2113
|
+
function describeZodIssues(error) {
|
|
2114
|
+
return error.issues.map((issue) => issue.path.length > 0 ? `${issue.path.join(".")}: ${issue.message}` : issue.message).join("; ");
|
|
2115
|
+
}
|
|
2116
|
+
function mergeSnapshot(graph, snapshot) {
|
|
2117
|
+
const exported = snapshot.graph;
|
|
2118
|
+
const incomingNodes = Array.isArray(exported.nodes) ? exported.nodes : [];
|
|
2119
|
+
const incomingEdges = Array.isArray(exported.edges) ? exported.edges : [];
|
|
2120
|
+
const issues = [];
|
|
2121
|
+
const validNodes = [];
|
|
2122
|
+
const validEdges = [];
|
|
2123
|
+
for (const node of incomingNodes) {
|
|
2124
|
+
if (node.attributes === void 0) continue;
|
|
2125
|
+
const parsed = GraphNodeSchema.safeParse(node.attributes);
|
|
2126
|
+
if (!parsed.success) {
|
|
2127
|
+
issues.push(`node "${node.key}": ${describeZodIssues(parsed.error)}`);
|
|
2128
|
+
continue;
|
|
2129
|
+
}
|
|
2130
|
+
validNodes.push({ key: node.key, attributes: parsed.data });
|
|
2131
|
+
}
|
|
2132
|
+
for (const edge of incomingEdges) {
|
|
2133
|
+
if (edge.attributes === void 0) continue;
|
|
2134
|
+
const parsed = GraphEdgeSchema.safeParse(edge.attributes);
|
|
2135
|
+
if (!parsed.success) {
|
|
2136
|
+
const label = edge.key ?? `${edge.source}->${edge.target}`;
|
|
2137
|
+
issues.push(`edge "${label}": ${describeZodIssues(parsed.error)}`);
|
|
2138
|
+
continue;
|
|
2139
|
+
}
|
|
2140
|
+
const id = edge.key ?? parsed.data.id;
|
|
2141
|
+
validEdges.push({ key: id, source: edge.source, target: edge.target, attributes: parsed.data });
|
|
2142
|
+
}
|
|
2143
|
+
if (issues.length > 0) {
|
|
2144
|
+
throw new SnapshotValidationError(issues);
|
|
2145
|
+
}
|
|
2146
|
+
let nodesAdded = 0;
|
|
2147
|
+
let edgesAdded = 0;
|
|
2148
|
+
for (const node of validNodes) {
|
|
2149
|
+
if (graph.hasNode(node.key)) continue;
|
|
2150
|
+
graph.addNode(node.key, node.attributes);
|
|
2151
|
+
nodesAdded++;
|
|
2152
|
+
}
|
|
2153
|
+
for (const edge of validEdges) {
|
|
2154
|
+
if (graph.hasEdge(edge.key)) continue;
|
|
2155
|
+
if (!graph.hasNode(edge.source) || !graph.hasNode(edge.target)) continue;
|
|
2156
|
+
graph.addEdgeWithKey(edge.key, edge.source, edge.target, edge.attributes);
|
|
2157
|
+
edgesAdded++;
|
|
2158
|
+
}
|
|
2159
|
+
return { nodesAdded, edgesAdded };
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
// src/traverse.ts
|
|
2163
|
+
import {
|
|
2164
|
+
BlastRadiusResultSchema,
|
|
2165
|
+
EdgeType as EdgeType3,
|
|
2166
|
+
NodeType as NodeType3,
|
|
2167
|
+
ObservedDependenciesResultSchema,
|
|
2168
|
+
PROV_RANK,
|
|
2169
|
+
Provenance as Provenance2,
|
|
2170
|
+
RootCauseResultSchema,
|
|
2171
|
+
TransitiveDependenciesResultSchema
|
|
2172
|
+
} from "@neat.is/types";
|
|
2173
|
+
var ROOT_CAUSE_MAX_DEPTH = 5;
|
|
2174
|
+
var BLAST_RADIUS_DEFAULT_DEPTH = 10;
|
|
2175
|
+
function isFrontierNode(graph, nodeId) {
|
|
2176
|
+
if (!graph.hasNode(nodeId)) return false;
|
|
2177
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
2178
|
+
return attrs.type === NodeType3.FrontierNode;
|
|
2179
|
+
}
|
|
2180
|
+
function resolveOwningService(graph, nodeId) {
|
|
2181
|
+
if (!graph.hasNode(nodeId)) return null;
|
|
2182
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
2183
|
+
if (attrs.type === NodeType3.ServiceNode) {
|
|
2184
|
+
return { id: nodeId, svc: attrs };
|
|
2185
|
+
}
|
|
2186
|
+
if (attrs.type === NodeType3.FileNode) {
|
|
2187
|
+
for (const edgeId of graph.inboundEdges(nodeId)) {
|
|
2188
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
2189
|
+
if (e.type !== EdgeType3.CONTAINS) continue;
|
|
2190
|
+
const owner = graph.getNodeAttributes(e.source);
|
|
2191
|
+
if (owner.type === NodeType3.ServiceNode) {
|
|
2192
|
+
return { id: e.source, svc: owner };
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
return null;
|
|
2197
|
+
}
|
|
2198
|
+
function bestEdgeBySource(graph, edgeIds) {
|
|
2199
|
+
const best = /* @__PURE__ */ new Map();
|
|
2200
|
+
for (const id of edgeIds) {
|
|
2201
|
+
const e = graph.getEdgeAttributes(id);
|
|
2202
|
+
if (isFrontierNode(graph, e.source)) continue;
|
|
2203
|
+
const cur = best.get(e.source);
|
|
2204
|
+
if (!cur || PROV_RANK[e.provenance] > PROV_RANK[cur.provenance]) {
|
|
2205
|
+
best.set(e.source, e);
|
|
2362
2206
|
}
|
|
2363
2207
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
errorMessage: incidentMessage(span),
|
|
2375
|
-
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2376
|
-
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2377
|
-
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
2378
|
-
affectedNode
|
|
2379
|
-
};
|
|
2380
|
-
await appendErrorEvent(ctx, ev);
|
|
2208
|
+
return best;
|
|
2209
|
+
}
|
|
2210
|
+
function bestEdgeByTarget(graph, edgeIds) {
|
|
2211
|
+
const best = /* @__PURE__ */ new Map();
|
|
2212
|
+
for (const id of edgeIds) {
|
|
2213
|
+
const e = graph.getEdgeAttributes(id);
|
|
2214
|
+
if (isFrontierNode(graph, e.target)) continue;
|
|
2215
|
+
const cur = best.get(e.target);
|
|
2216
|
+
if (!cur || PROV_RANK[e.provenance] > PROV_RANK[cur.provenance]) {
|
|
2217
|
+
best.set(e.target, e);
|
|
2381
2218
|
}
|
|
2382
2219
|
}
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2220
|
+
return best;
|
|
2221
|
+
}
|
|
2222
|
+
var PROVENANCE_CEILING = {
|
|
2223
|
+
OBSERVED: 1,
|
|
2224
|
+
INFERRED: 0.7,
|
|
2225
|
+
EXTRACTED: 0.5,
|
|
2226
|
+
STALE: 0.3
|
|
2227
|
+
};
|
|
2228
|
+
function volumeWeight(spanCount) {
|
|
2229
|
+
if (!spanCount || spanCount <= 0) return 0.5;
|
|
2230
|
+
const w = 0.5 + Math.log10(spanCount + 1) / 3;
|
|
2231
|
+
return Math.min(1, w);
|
|
2232
|
+
}
|
|
2233
|
+
function recencyWeight(ageMs) {
|
|
2234
|
+
if (ageMs === void 0) return 0.8;
|
|
2235
|
+
const hour = 60 * 60 * 1e3;
|
|
2236
|
+
if (ageMs <= hour) return 1;
|
|
2237
|
+
if (ageMs <= 24 * hour) {
|
|
2238
|
+
const t = (ageMs - hour) / (23 * hour);
|
|
2239
|
+
return 1 - 0.5 * t;
|
|
2240
|
+
}
|
|
2241
|
+
return 0.3;
|
|
2242
|
+
}
|
|
2243
|
+
function cleanlinessWeight(spanCount, errorCount) {
|
|
2244
|
+
if (!spanCount || spanCount <= 0) return 1;
|
|
2245
|
+
const rate = (errorCount ?? 0) / spanCount;
|
|
2246
|
+
if (rate <= 0.01) return 1;
|
|
2247
|
+
if (rate >= 0.5) return 0.3;
|
|
2248
|
+
return 1 - rate * 1.4;
|
|
2249
|
+
}
|
|
2250
|
+
function confidenceForEdge(edge, now = Date.now()) {
|
|
2251
|
+
const ceiling = PROVENANCE_CEILING[edge.provenance] ?? 0.5;
|
|
2252
|
+
const spanCount = edge.signal?.spanCount ?? edge.callCount;
|
|
2253
|
+
const ageMs = edge.signal?.lastObservedAgeMs ?? lastObservedAge(edge, now);
|
|
2254
|
+
if (spanCount === void 0 && ageMs === void 0 && edge.signal === void 0) {
|
|
2255
|
+
return ceiling;
|
|
2256
|
+
}
|
|
2257
|
+
const v = volumeWeight(spanCount);
|
|
2258
|
+
const r = recencyWeight(ageMs);
|
|
2259
|
+
const c = cleanlinessWeight(spanCount, edge.signal?.errorCount);
|
|
2260
|
+
return Math.max(0, Math.min(1, ceiling * v * r * c));
|
|
2261
|
+
}
|
|
2262
|
+
function lastObservedAge(edge, now) {
|
|
2263
|
+
if (!edge.lastObserved) return void 0;
|
|
2264
|
+
const t = Date.parse(edge.lastObserved);
|
|
2265
|
+
if (!Number.isFinite(t)) return void 0;
|
|
2266
|
+
return Math.max(0, now - t);
|
|
2267
|
+
}
|
|
2268
|
+
function confidenceFromMix(edges, now = Date.now()) {
|
|
2269
|
+
if (edges.length === 0) return 1;
|
|
2270
|
+
let product = 1;
|
|
2271
|
+
for (const e of edges) {
|
|
2272
|
+
product *= confidenceForEdge(e, now);
|
|
2273
|
+
}
|
|
2274
|
+
return Math.max(0, Math.min(1, product));
|
|
2275
|
+
}
|
|
2276
|
+
function longestIncomingWalk(graph, start, maxDepth) {
|
|
2277
|
+
let best = { path: [start], edges: [] };
|
|
2278
|
+
const visited = /* @__PURE__ */ new Set([start]);
|
|
2279
|
+
function step(node, path49, edges) {
|
|
2280
|
+
if (path49.length > best.path.length) {
|
|
2281
|
+
best = { path: [...path49], edges: [...edges] };
|
|
2282
|
+
}
|
|
2283
|
+
if (path49.length - 1 >= maxDepth) return;
|
|
2284
|
+
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
2285
|
+
for (const [srcId, edge] of incoming) {
|
|
2286
|
+
if (visited.has(srcId)) continue;
|
|
2287
|
+
visited.add(srcId);
|
|
2288
|
+
path49.push(srcId);
|
|
2289
|
+
edges.push(edge);
|
|
2290
|
+
step(srcId, path49, edges);
|
|
2291
|
+
path49.pop();
|
|
2292
|
+
edges.pop();
|
|
2293
|
+
visited.delete(srcId);
|
|
2391
2294
|
}
|
|
2392
2295
|
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2296
|
+
step(start, [start], []);
|
|
2297
|
+
return best;
|
|
2395
2298
|
}
|
|
2396
|
-
function
|
|
2397
|
-
const
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
if (
|
|
2403
|
-
|
|
2299
|
+
function databaseRootCauseShape(graph, origin, walk4) {
|
|
2300
|
+
const targetDb = origin;
|
|
2301
|
+
const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
|
|
2302
|
+
if (candidatePairs.length === 0) return null;
|
|
2303
|
+
for (const id of walk4.path) {
|
|
2304
|
+
const owner = resolveOwningService(graph, id);
|
|
2305
|
+
if (!owner) continue;
|
|
2306
|
+
const { id: serviceId5, svc } = owner;
|
|
2307
|
+
const deps = svc.dependencies ?? {};
|
|
2308
|
+
for (const pair of candidatePairs) {
|
|
2309
|
+
const declared = deps[pair.driver];
|
|
2310
|
+
if (!declared) continue;
|
|
2311
|
+
const result = checkCompatibility(
|
|
2312
|
+
pair.driver,
|
|
2313
|
+
declared,
|
|
2314
|
+
targetDb.engine,
|
|
2315
|
+
targetDb.engineVersion
|
|
2316
|
+
);
|
|
2317
|
+
if (!result.compatible) {
|
|
2318
|
+
return {
|
|
2319
|
+
rootCauseNode: serviceId5,
|
|
2320
|
+
rootCauseReason: result.reason ?? "incompatible driver",
|
|
2321
|
+
...result.minDriverVersion ? {
|
|
2322
|
+
fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
|
|
2323
|
+
} : {}
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2404
2326
|
}
|
|
2405
|
-
}
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
const
|
|
2411
|
-
if (!
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
const
|
|
2419
|
-
if (!
|
|
2420
|
-
|
|
2327
|
+
}
|
|
2328
|
+
return null;
|
|
2329
|
+
}
|
|
2330
|
+
function serviceRootCauseShape(graph, _origin, walk4) {
|
|
2331
|
+
for (const id of walk4.path) {
|
|
2332
|
+
const owner = resolveOwningService(graph, id);
|
|
2333
|
+
if (!owner) continue;
|
|
2334
|
+
const { id: serviceId5, svc } = owner;
|
|
2335
|
+
const deps = svc.dependencies ?? {};
|
|
2336
|
+
const serviceNodeEngine = svc.nodeEngine;
|
|
2337
|
+
for (const constraint of nodeEngineConstraints()) {
|
|
2338
|
+
const declared = deps[constraint.package];
|
|
2339
|
+
if (!declared) continue;
|
|
2340
|
+
const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
|
|
2341
|
+
if (!result.compatible && result.reason) {
|
|
2342
|
+
return {
|
|
2343
|
+
rootCauseNode: serviceId5,
|
|
2344
|
+
rootCauseReason: result.reason,
|
|
2345
|
+
...result.requiredNodeVersion ? {
|
|
2346
|
+
fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
|
|
2347
|
+
} : {}
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
for (const conflict of packageConflicts()) {
|
|
2352
|
+
const declared = deps[conflict.package];
|
|
2353
|
+
if (!declared) continue;
|
|
2354
|
+
const requiredDeclared = deps[conflict.requires.name];
|
|
2355
|
+
const result = checkPackageConflict(conflict, declared, requiredDeclared);
|
|
2356
|
+
if (!result.compatible && result.reason) {
|
|
2357
|
+
return {
|
|
2358
|
+
rootCauseNode: serviceId5,
|
|
2359
|
+
rootCauseReason: result.reason,
|
|
2360
|
+
fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
|
|
2361
|
+
};
|
|
2421
2362
|
}
|
|
2422
2363
|
}
|
|
2423
|
-
rewireFrontierEdges(graph, frontierId2, serviceId5);
|
|
2424
|
-
graph.dropNode(frontierId2);
|
|
2425
|
-
promoted++;
|
|
2426
2364
|
}
|
|
2427
|
-
return
|
|
2365
|
+
return null;
|
|
2428
2366
|
}
|
|
2429
|
-
function
|
|
2430
|
-
const
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2367
|
+
function fileRootCauseShape(graph, origin, walk4) {
|
|
2368
|
+
const owner = resolveOwningService(graph, origin.id);
|
|
2369
|
+
if (!owner) return null;
|
|
2370
|
+
return serviceRootCauseShape(graph, owner.svc, walk4);
|
|
2371
|
+
}
|
|
2372
|
+
var rootCauseShapes = {
|
|
2373
|
+
[NodeType3.DatabaseNode]: databaseRootCauseShape,
|
|
2374
|
+
[NodeType3.ServiceNode]: serviceRootCauseShape,
|
|
2375
|
+
[NodeType3.FileNode]: fileRootCauseShape
|
|
2376
|
+
};
|
|
2377
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
2378
|
+
if (!graph.hasNode(errorNodeId)) return null;
|
|
2379
|
+
const origin = graph.getNodeAttributes(errorNodeId);
|
|
2380
|
+
const shape = rootCauseShapes[origin.type];
|
|
2381
|
+
if (shape) {
|
|
2382
|
+
const walk4 = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
|
|
2383
|
+
const match = shape(graph, origin, walk4);
|
|
2384
|
+
if (match) {
|
|
2385
|
+
const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
|
|
2386
|
+
return RootCauseResultSchema.parse({
|
|
2387
|
+
rootCauseNode: match.rootCauseNode,
|
|
2388
|
+
rootCauseReason: reason,
|
|
2389
|
+
traversalPath: walk4.path,
|
|
2390
|
+
edgeProvenances: walk4.edges.map((e) => e.provenance),
|
|
2391
|
+
confidence: confidenceFromMix(walk4.edges),
|
|
2392
|
+
fixRecommendation: match.fixRecommendation
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2435
2395
|
}
|
|
2436
|
-
|
|
2437
|
-
const
|
|
2438
|
-
|
|
2396
|
+
if (origin.type === NodeType3.ServiceNode) {
|
|
2397
|
+
const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
|
|
2398
|
+
if (crossService) return crossService;
|
|
2439
2399
|
}
|
|
2400
|
+
return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
|
|
2440
2401
|
}
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
const
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2402
|
+
var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
|
|
2403
|
+
function incidentMatchesNode(ev, nodeId) {
|
|
2404
|
+
return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
|
|
2405
|
+
}
|
|
2406
|
+
function localizeFromIncidents(nodeId, incidents, errorEvent) {
|
|
2407
|
+
const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
|
|
2408
|
+
const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
|
|
2409
|
+
if (relevant.length === 0) return null;
|
|
2410
|
+
const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
|
|
2411
|
+
const attrs = latest.attributes ?? {};
|
|
2412
|
+
const filepath = codeFilepathOf(attrs);
|
|
2413
|
+
const lineno = codeLinenoOf(attrs);
|
|
2414
|
+
const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
|
|
2415
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
2416
|
+
const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
|
|
2417
|
+
const count = sameMode.length;
|
|
2418
|
+
const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
|
|
2419
|
+
const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
|
|
2420
|
+
if (location) reasonParts.push(`surfaced at ${location}`);
|
|
2421
|
+
const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
|
|
2422
|
+
const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
|
|
2423
|
+
const fileNode = localizesToFile ? latest.affectedNode : void 0;
|
|
2424
|
+
const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
|
|
2425
|
+
return {
|
|
2426
|
+
rootCauseNode: fileNode ?? nodeId,
|
|
2427
|
+
rootCauseReason,
|
|
2428
|
+
...fileNode ? { fileNode } : {},
|
|
2429
|
+
...fixRecommendation ? { fixRecommendation } : {}
|
|
2459
2430
|
};
|
|
2460
|
-
graph.addEdgeWithKey(newId, newSource, newTarget, rebuilt);
|
|
2461
2431
|
}
|
|
2462
|
-
function
|
|
2463
|
-
|
|
2464
|
-
if (!
|
|
2465
|
-
|
|
2432
|
+
function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
2433
|
+
const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
|
|
2434
|
+
if (!loc) return null;
|
|
2435
|
+
const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
|
|
2436
|
+
const edgeProvenances = loc.fileNode ? [Provenance2.OBSERVED] : [];
|
|
2437
|
+
return RootCauseResultSchema.parse({
|
|
2438
|
+
rootCauseNode: loc.rootCauseNode,
|
|
2439
|
+
rootCauseReason: loc.rootCauseReason,
|
|
2440
|
+
traversalPath,
|
|
2441
|
+
edgeProvenances,
|
|
2442
|
+
confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
|
|
2443
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
2444
|
+
});
|
|
2466
2445
|
}
|
|
2467
|
-
function
|
|
2468
|
-
return
|
|
2446
|
+
function isFailingCallEdge(e) {
|
|
2447
|
+
return e.type === EdgeType3.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
2469
2448
|
}
|
|
2470
|
-
|
|
2471
|
-
const
|
|
2472
|
-
const
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
if (e.provenance !== Provenance2.OBSERVED) return;
|
|
2478
|
-
if (!e.lastObserved) return;
|
|
2479
|
-
const threshold = thresholdForEdgeType(e.type, thresholds);
|
|
2480
|
-
const age = now - new Date(e.lastObserved).getTime();
|
|
2481
|
-
if (age > threshold) {
|
|
2482
|
-
const updated = { ...e, provenance: Provenance2.STALE, confidence: 0.3 };
|
|
2483
|
-
graph.replaceEdgeAttributes(id, updated);
|
|
2484
|
-
events.push({
|
|
2485
|
-
edgeId: id,
|
|
2486
|
-
source: e.source,
|
|
2487
|
-
target: e.target,
|
|
2488
|
-
edgeType: e.type,
|
|
2489
|
-
thresholdMs: threshold,
|
|
2490
|
-
ageMs: age,
|
|
2491
|
-
lastObserved: e.lastObserved,
|
|
2492
|
-
transitionedAt: new Date(now).toISOString()
|
|
2493
|
-
});
|
|
2494
|
-
emitNeatEvent({
|
|
2495
|
-
type: "stale-transition",
|
|
2496
|
-
project,
|
|
2497
|
-
payload: {
|
|
2498
|
-
edgeId: id,
|
|
2499
|
-
from: Provenance2.OBSERVED,
|
|
2500
|
-
to: Provenance2.STALE
|
|
2501
|
-
}
|
|
2502
|
-
});
|
|
2503
|
-
}
|
|
2504
|
-
});
|
|
2505
|
-
if (options.staleEventsPath && events.length > 0) {
|
|
2506
|
-
await appendStaleEvents(options.staleEventsPath, events);
|
|
2449
|
+
function callSourcesForService(graph, serviceId5) {
|
|
2450
|
+
const ids = [serviceId5];
|
|
2451
|
+
for (const edgeId of graph.outboundEdges(serviceId5)) {
|
|
2452
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
2453
|
+
if (e.type !== EdgeType3.CONTAINS) continue;
|
|
2454
|
+
const tgt = graph.getNodeAttributes(e.target);
|
|
2455
|
+
if (tgt.type === NodeType3.FileNode) ids.push(e.target);
|
|
2507
2456
|
}
|
|
2508
|
-
return
|
|
2509
|
-
}
|
|
2510
|
-
async function appendStaleEvents(staleEventsPath, events) {
|
|
2511
|
-
await fs3.mkdir(path3.dirname(staleEventsPath), { recursive: true });
|
|
2512
|
-
const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2513
|
-
await fs3.appendFile(staleEventsPath, lines, "utf8");
|
|
2457
|
+
return ids;
|
|
2514
2458
|
}
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
throw err;
|
|
2459
|
+
function failingCallDominates(e, id, curEdge, curId) {
|
|
2460
|
+
const ec = e.signal?.errorCount ?? 0;
|
|
2461
|
+
const cc = curEdge.signal?.errorCount ?? 0;
|
|
2462
|
+
if (ec !== cc) return ec > cc;
|
|
2463
|
+
if (PROV_RANK[e.provenance] !== PROV_RANK[curEdge.provenance]) {
|
|
2464
|
+
return PROV_RANK[e.provenance] > PROV_RANK[curEdge.provenance];
|
|
2522
2465
|
}
|
|
2466
|
+
return id < curId;
|
|
2523
2467
|
}
|
|
2524
|
-
function
|
|
2525
|
-
let
|
|
2526
|
-
const
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
});
|
|
2536
|
-
if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
|
|
2537
|
-
} catch (err) {
|
|
2538
|
-
console.error("staleness tick failed", err);
|
|
2468
|
+
function dominantFailingCall(graph, serviceId5, visited) {
|
|
2469
|
+
let best = null;
|
|
2470
|
+
for (const src of callSourcesForService(graph, serviceId5)) {
|
|
2471
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
2472
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
2473
|
+
if (!isFailingCallEdge(e)) continue;
|
|
2474
|
+
if (isFrontierNode(graph, e.target)) continue;
|
|
2475
|
+
const owner = resolveOwningService(graph, e.target);
|
|
2476
|
+
if (!owner || visited.has(owner.id)) continue;
|
|
2477
|
+
if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
|
|
2478
|
+
best = { nextService: owner.id, edge: e };
|
|
2539
2479
|
}
|
|
2540
|
-
}
|
|
2541
|
-
}
|
|
2542
|
-
|
|
2543
|
-
if (typeof interval.unref === "function") interval.unref();
|
|
2544
|
-
return () => {
|
|
2545
|
-
stopped = true;
|
|
2546
|
-
clearInterval(interval);
|
|
2547
|
-
};
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
return best;
|
|
2548
2483
|
}
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2484
|
+
function followFailingCallChain(graph, originServiceId, maxDepth) {
|
|
2485
|
+
const path49 = [originServiceId];
|
|
2486
|
+
const edges = [];
|
|
2487
|
+
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
2488
|
+
let current = originServiceId;
|
|
2489
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
2490
|
+
const hop = dominantFailingCall(graph, current, visited);
|
|
2491
|
+
if (!hop) break;
|
|
2492
|
+
path49.push(hop.nextService);
|
|
2493
|
+
edges.push(hop.edge);
|
|
2494
|
+
visited.add(hop.nextService);
|
|
2495
|
+
current = hop.nextService;
|
|
2496
|
+
}
|
|
2497
|
+
if (edges.length === 0) return null;
|
|
2498
|
+
return { path: path49, edges, culprit: current };
|
|
2499
|
+
}
|
|
2500
|
+
function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
2501
|
+
const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
|
|
2502
|
+
if (!chain) return null;
|
|
2503
|
+
const culprit = chain.culprit;
|
|
2504
|
+
const path49 = [...chain.path];
|
|
2505
|
+
const edgeProvenances = chain.edges.map((e) => e.provenance);
|
|
2506
|
+
const baseConfidence = confidenceFromMix(chain.edges);
|
|
2507
|
+
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
2508
|
+
const loc = localizeFromIncidents(culprit, incidents, errorEvent);
|
|
2509
|
+
if (loc) {
|
|
2510
|
+
let rootCauseNode = culprit;
|
|
2511
|
+
if (loc.fileNode) {
|
|
2512
|
+
path49.push(loc.fileNode);
|
|
2513
|
+
edgeProvenances.push(Provenance2.OBSERVED);
|
|
2514
|
+
rootCauseNode = loc.fileNode;
|
|
2515
|
+
}
|
|
2516
|
+
return RootCauseResultSchema.parse({
|
|
2517
|
+
rootCauseNode,
|
|
2518
|
+
rootCauseReason: loc.rootCauseReason,
|
|
2519
|
+
traversalPath: path49,
|
|
2520
|
+
edgeProvenances,
|
|
2521
|
+
confidence,
|
|
2522
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
2523
|
+
});
|
|
2524
|
+
}
|
|
2525
|
+
const lastEdge = chain.edges[chain.edges.length - 1];
|
|
2526
|
+
const errs = lastEdge.signal?.errorCount ?? 0;
|
|
2527
|
+
const culpritName = culprit.replace(/^service:/, "");
|
|
2528
|
+
return RootCauseResultSchema.parse({
|
|
2529
|
+
rootCauseNode: culprit,
|
|
2530
|
+
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
2531
|
+
traversalPath: path49,
|
|
2532
|
+
edgeProvenances,
|
|
2533
|
+
confidence,
|
|
2534
|
+
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
2535
|
+
});
|
|
2536
|
+
}
|
|
2537
|
+
function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
2538
|
+
if (!graph.hasNode(nodeId)) {
|
|
2539
|
+
return BlastRadiusResultSchema.parse({ origin: nodeId, affectedNodes: [], totalAffected: 0 });
|
|
2540
|
+
}
|
|
2541
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2542
|
+
const queue = [{ nodeId, distance: 0, path: [nodeId], pathEdges: [] }];
|
|
2543
|
+
const enqueued = /* @__PURE__ */ new Set([nodeId]);
|
|
2544
|
+
while (queue.length > 0) {
|
|
2545
|
+
const frame = queue.shift();
|
|
2546
|
+
if (frame.distance > 0 && frame.pathEdges.length > 0) {
|
|
2547
|
+
const lastEdge = frame.pathEdges[frame.pathEdges.length - 1];
|
|
2548
|
+
seen.set(frame.nodeId, {
|
|
2549
|
+
nodeId: frame.nodeId,
|
|
2550
|
+
distance: frame.distance,
|
|
2551
|
+
edgeProvenance: lastEdge.provenance,
|
|
2552
|
+
path: frame.path,
|
|
2553
|
+
confidence: confidenceFromMix(frame.pathEdges)
|
|
2554
|
+
});
|
|
2555
|
+
}
|
|
2556
|
+
if (frame.distance >= maxDepth) continue;
|
|
2557
|
+
const incoming = bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId));
|
|
2558
|
+
for (const [srcId, edge] of incoming) {
|
|
2559
|
+
if (enqueued.has(srcId)) continue;
|
|
2560
|
+
enqueued.add(srcId);
|
|
2561
|
+
queue.push({
|
|
2562
|
+
nodeId: srcId,
|
|
2563
|
+
distance: frame.distance + 1,
|
|
2564
|
+
path: [...frame.path, srcId],
|
|
2565
|
+
pathEdges: [...frame.pathEdges, edge]
|
|
2566
|
+
});
|
|
2567
|
+
}
|
|
2557
2568
|
}
|
|
2569
|
+
const affectedNodes = [...seen.values()].sort(
|
|
2570
|
+
(a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
|
|
2571
|
+
);
|
|
2572
|
+
return BlastRadiusResultSchema.parse({
|
|
2573
|
+
origin: nodeId,
|
|
2574
|
+
affectedNodes,
|
|
2575
|
+
totalAffected: affectedNodes.length
|
|
2576
|
+
});
|
|
2558
2577
|
}
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
if (!
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
for (const ev of events) {
|
|
2570
|
-
const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
|
|
2571
|
-
if (key === void 0) {
|
|
2572
|
-
once.push(ev);
|
|
2573
|
-
continue;
|
|
2574
|
-
}
|
|
2575
|
-
if (seen.has(key)) continue;
|
|
2576
|
-
seen.add(key);
|
|
2577
|
-
once.push(ev);
|
|
2578
|
+
var TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH = 3;
|
|
2579
|
+
var TRANSITIVE_DEPENDENCIES_MAX_DEPTH = 10;
|
|
2580
|
+
function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH) {
|
|
2581
|
+
if (!graph.hasNode(nodeId)) {
|
|
2582
|
+
return TransitiveDependenciesResultSchema.parse({
|
|
2583
|
+
origin: nodeId,
|
|
2584
|
+
depth,
|
|
2585
|
+
dependencies: [],
|
|
2586
|
+
total: 0
|
|
2587
|
+
});
|
|
2578
2588
|
}
|
|
2579
|
-
const
|
|
2580
|
-
const
|
|
2581
|
-
|
|
2582
|
-
|
|
2589
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2590
|
+
const queue = [{ nodeId, distance: 0, edge: null }];
|
|
2591
|
+
const enqueued = /* @__PURE__ */ new Set([nodeId]);
|
|
2592
|
+
while (queue.length > 0) {
|
|
2593
|
+
const frame = queue.shift();
|
|
2594
|
+
if (frame.distance > 0 && frame.edge && frame.edge.type !== EdgeType3.CONTAINS) {
|
|
2595
|
+
seen.set(frame.nodeId, {
|
|
2596
|
+
nodeId: frame.nodeId,
|
|
2597
|
+
distance: frame.distance,
|
|
2598
|
+
edgeType: frame.edge.type,
|
|
2599
|
+
provenance: frame.edge.provenance
|
|
2600
|
+
});
|
|
2601
|
+
}
|
|
2602
|
+
if (frame.distance >= depth) continue;
|
|
2603
|
+
const outgoing = bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
|
|
2604
|
+
for (const [tgtId, edge] of outgoing) {
|
|
2605
|
+
if (enqueued.has(tgtId)) continue;
|
|
2606
|
+
enqueued.add(tgtId);
|
|
2607
|
+
queue.push({ nodeId: tgtId, distance: frame.distance + 1, edge });
|
|
2608
|
+
}
|
|
2583
2609
|
}
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2610
|
+
const dependencies = [...seen.values()].sort(
|
|
2611
|
+
(a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
|
|
2612
|
+
);
|
|
2613
|
+
return TransitiveDependenciesResultSchema.parse({
|
|
2614
|
+
origin: nodeId,
|
|
2615
|
+
depth,
|
|
2616
|
+
dependencies,
|
|
2617
|
+
total: dependencies.length
|
|
2587
2618
|
});
|
|
2588
2619
|
}
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2620
|
+
function getObservedDependencies(graph, nodeId) {
|
|
2621
|
+
if (!graph.hasNode(nodeId)) {
|
|
2622
|
+
return ObservedDependenciesResultSchema.parse({
|
|
2623
|
+
origin: nodeId,
|
|
2624
|
+
dependencies: [],
|
|
2625
|
+
observed: false,
|
|
2626
|
+
inboundObservedCount: 0,
|
|
2627
|
+
hasExtractedOutbound: false
|
|
2628
|
+
});
|
|
2594
2629
|
}
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
const incomingEdges = Array.isArray(exported.edges) ? exported.edges : [];
|
|
2604
|
-
const issues = [];
|
|
2605
|
-
const validNodes = [];
|
|
2606
|
-
const validEdges = [];
|
|
2607
|
-
for (const node of incomingNodes) {
|
|
2608
|
-
if (node.attributes === void 0) continue;
|
|
2609
|
-
const parsed = GraphNodeSchema.safeParse(node.attributes);
|
|
2610
|
-
if (!parsed.success) {
|
|
2611
|
-
issues.push(`node "${node.key}": ${describeZodIssues(parsed.error)}`);
|
|
2612
|
-
continue;
|
|
2630
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
2631
|
+
const scope = [nodeId];
|
|
2632
|
+
if (attrs.type === NodeType3.ServiceNode) {
|
|
2633
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
2634
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
2635
|
+
if (e.type !== EdgeType3.CONTAINS) continue;
|
|
2636
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
2637
|
+
if (owned.type === NodeType3.FileNode) scope.push(e.target);
|
|
2613
2638
|
}
|
|
2614
|
-
validNodes.push({ key: node.key, attributes: parsed.data });
|
|
2615
2639
|
}
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
continue;
|
|
2640
|
+
const dependencies = [];
|
|
2641
|
+
const seenEdge = /* @__PURE__ */ new Set();
|
|
2642
|
+
let hasExtractedOutbound = false;
|
|
2643
|
+
for (const src of scope) {
|
|
2644
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
2645
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
2646
|
+
if (e.type === EdgeType3.CONTAINS) continue;
|
|
2647
|
+
if (e.provenance === Provenance2.OBSERVED) {
|
|
2648
|
+
if (!seenEdge.has(e.id)) {
|
|
2649
|
+
seenEdge.add(e.id);
|
|
2650
|
+
dependencies.push(e);
|
|
2651
|
+
}
|
|
2652
|
+
} else if (e.provenance === Provenance2.EXTRACTED) {
|
|
2653
|
+
hasExtractedOutbound = true;
|
|
2654
|
+
}
|
|
2623
2655
|
}
|
|
2624
|
-
const id = edge.key ?? parsed.data.id;
|
|
2625
|
-
validEdges.push({ key: id, source: edge.source, target: edge.target, attributes: parsed.data });
|
|
2626
|
-
}
|
|
2627
|
-
if (issues.length > 0) {
|
|
2628
|
-
throw new SnapshotValidationError(issues);
|
|
2629
|
-
}
|
|
2630
|
-
let nodesAdded = 0;
|
|
2631
|
-
let edgesAdded = 0;
|
|
2632
|
-
for (const node of validNodes) {
|
|
2633
|
-
if (graph.hasNode(node.key)) continue;
|
|
2634
|
-
graph.addNode(node.key, node.attributes);
|
|
2635
|
-
nodesAdded++;
|
|
2636
2656
|
}
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2657
|
+
let inboundObservedCount = 0;
|
|
2658
|
+
for (const tgt of scope) {
|
|
2659
|
+
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
2660
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
2661
|
+
if (e.type === EdgeType3.CONTAINS) continue;
|
|
2662
|
+
if (e.provenance === Provenance2.OBSERVED) inboundObservedCount += 1;
|
|
2663
|
+
}
|
|
2642
2664
|
}
|
|
2643
|
-
|
|
2665
|
+
dependencies.sort(
|
|
2666
|
+
(a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
|
|
2667
|
+
);
|
|
2668
|
+
return ObservedDependenciesResultSchema.parse({
|
|
2669
|
+
origin: nodeId,
|
|
2670
|
+
dependencies,
|
|
2671
|
+
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
2672
|
+
inboundObservedCount,
|
|
2673
|
+
hasExtractedOutbound
|
|
2674
|
+
});
|
|
2644
2675
|
}
|
|
2645
2676
|
|
|
2646
2677
|
// src/extract/errors.ts
|
|
@@ -3417,14 +3448,14 @@ function buildServiceHostIndex(services) {
|
|
|
3417
3448
|
}
|
|
3418
3449
|
async function walkSourceFiles(dir) {
|
|
3419
3450
|
const out = [];
|
|
3420
|
-
async function
|
|
3451
|
+
async function walk4(current) {
|
|
3421
3452
|
const entries = await fs10.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
3422
3453
|
for (const entry of entries) {
|
|
3423
3454
|
const full = path10.join(current, entry.name);
|
|
3424
3455
|
if (entry.isDirectory()) {
|
|
3425
3456
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3426
3457
|
if (await isPythonVenvDir(full)) continue;
|
|
3427
|
-
await
|
|
3458
|
+
await walk4(full);
|
|
3428
3459
|
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
|
|
3429
3460
|
// would attribute our instrumentation imports to the user's service.
|
|
3430
3461
|
!isNeatAuthoredSourceFile(entry.name)) {
|
|
@@ -3432,7 +3463,7 @@ async function walkSourceFiles(dir) {
|
|
|
3432
3463
|
}
|
|
3433
3464
|
}
|
|
3434
3465
|
}
|
|
3435
|
-
await
|
|
3466
|
+
await walk4(dir);
|
|
3436
3467
|
return out;
|
|
3437
3468
|
}
|
|
3438
3469
|
async function loadSourceFiles(dir) {
|
|
@@ -4588,20 +4619,20 @@ import {
|
|
|
4588
4619
|
} from "@neat.is/types";
|
|
4589
4620
|
async function walkConfigFiles(dir) {
|
|
4590
4621
|
const out = [];
|
|
4591
|
-
async function
|
|
4622
|
+
async function walk4(current) {
|
|
4592
4623
|
const entries = await fs14.readdir(current, { withFileTypes: true });
|
|
4593
4624
|
for (const entry of entries) {
|
|
4594
4625
|
const full = path21.join(current, entry.name);
|
|
4595
4626
|
if (entry.isDirectory()) {
|
|
4596
4627
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
4597
4628
|
if (await isPythonVenvDir(full)) continue;
|
|
4598
|
-
await
|
|
4629
|
+
await walk4(full);
|
|
4599
4630
|
} else if (entry.isFile() && isConfigFile(entry.name).match) {
|
|
4600
4631
|
out.push(full);
|
|
4601
4632
|
}
|
|
4602
4633
|
}
|
|
4603
4634
|
}
|
|
4604
|
-
await
|
|
4635
|
+
await walk4(dir);
|
|
4605
4636
|
return out;
|
|
4606
4637
|
}
|
|
4607
4638
|
async function addConfigNodes(graph, services, scanPath) {
|
|
@@ -5140,20 +5171,20 @@ function grpcMethodsFromProto(content, fqPackage) {
|
|
|
5140
5171
|
}
|
|
5141
5172
|
async function walkProtoFiles(dir) {
|
|
5142
5173
|
const out = [];
|
|
5143
|
-
async function
|
|
5174
|
+
async function walk4(current) {
|
|
5144
5175
|
const entries = await fs15.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
5145
5176
|
for (const entry of entries) {
|
|
5146
5177
|
const full = path23.join(current, entry.name);
|
|
5147
5178
|
if (entry.isDirectory()) {
|
|
5148
5179
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
5149
5180
|
if (await isPythonVenvDir(full)) continue;
|
|
5150
|
-
await
|
|
5181
|
+
await walk4(full);
|
|
5151
5182
|
} else if (entry.isFile() && path23.extname(entry.name) === PROTO_EXTENSION) {
|
|
5152
5183
|
out.push(full);
|
|
5153
5184
|
}
|
|
5154
5185
|
}
|
|
5155
5186
|
}
|
|
5156
|
-
await
|
|
5187
|
+
await walk4(dir);
|
|
5157
5188
|
return out;
|
|
5158
5189
|
}
|
|
5159
5190
|
async function addGrpcMethods(graph, services) {
|
|
@@ -6239,6 +6270,120 @@ async function mongooseCrossFileEndpoints(files, serviceDir) {
|
|
|
6239
6270
|
return out;
|
|
6240
6271
|
}
|
|
6241
6272
|
|
|
6273
|
+
// src/extract/calls/sqlalchemy.ts
|
|
6274
|
+
import path32 from "path";
|
|
6275
|
+
import Parser5 from "tree-sitter";
|
|
6276
|
+
import Python4 from "tree-sitter-python";
|
|
6277
|
+
import { infraId as infraId8 } from "@neat.is/types";
|
|
6278
|
+
var SQLALCHEMY_IMPORT_RE = /(?:from|import)\s+(?:flask_sqlalchemy|sqlalchemy)\b/;
|
|
6279
|
+
var PARSE_CHUNK5 = 16384;
|
|
6280
|
+
function makePyParser4() {
|
|
6281
|
+
const p = new Parser5();
|
|
6282
|
+
p.setLanguage(Python4);
|
|
6283
|
+
return p;
|
|
6284
|
+
}
|
|
6285
|
+
function parseSource5(parser, source) {
|
|
6286
|
+
return parser.parse(
|
|
6287
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK5)
|
|
6288
|
+
);
|
|
6289
|
+
}
|
|
6290
|
+
function flaskSqlalchemyTableName(className) {
|
|
6291
|
+
return className.replace(/((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))/g, "_$1").toLowerCase().replace(/^_+/, "");
|
|
6292
|
+
}
|
|
6293
|
+
function namedChildren(node) {
|
|
6294
|
+
const out = [];
|
|
6295
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
6296
|
+
const c = node.namedChild(i);
|
|
6297
|
+
if (c) out.push(c);
|
|
6298
|
+
}
|
|
6299
|
+
return out;
|
|
6300
|
+
}
|
|
6301
|
+
function pyStaticStringText2(node) {
|
|
6302
|
+
if (node.type !== "string") return null;
|
|
6303
|
+
for (const child of namedChildren(node)) {
|
|
6304
|
+
if (child.type === "interpolation") return null;
|
|
6305
|
+
if (child.type === "string_content") return child.text;
|
|
6306
|
+
}
|
|
6307
|
+
return "";
|
|
6308
|
+
}
|
|
6309
|
+
function explicitTablename(body) {
|
|
6310
|
+
for (const stmt of namedChildren(body)) {
|
|
6311
|
+
if (stmt.type !== "expression_statement") continue;
|
|
6312
|
+
const assign = stmt.namedChild(0);
|
|
6313
|
+
if (assign?.type !== "assignment") continue;
|
|
6314
|
+
if (assign.childForFieldName("left")?.text !== "__tablename__") continue;
|
|
6315
|
+
const right = assign.childForFieldName("right");
|
|
6316
|
+
if (right?.type === "string") {
|
|
6317
|
+
const s = pyStaticStringText2(right);
|
|
6318
|
+
return s ? { name: s } : "computed";
|
|
6319
|
+
}
|
|
6320
|
+
return "computed";
|
|
6321
|
+
}
|
|
6322
|
+
return null;
|
|
6323
|
+
}
|
|
6324
|
+
function extendsFlaskModel(cls) {
|
|
6325
|
+
const supers = cls.childForFieldName("superclasses");
|
|
6326
|
+
if (!supers) return false;
|
|
6327
|
+
for (const a of namedChildren(supers)) {
|
|
6328
|
+
const t = a.text;
|
|
6329
|
+
if (t === "db.Model" || t === "Model" || t.endsWith(".Model")) return true;
|
|
6330
|
+
}
|
|
6331
|
+
return false;
|
|
6332
|
+
}
|
|
6333
|
+
function walk3(node, visit) {
|
|
6334
|
+
visit(node);
|
|
6335
|
+
for (const c of namedChildren(node)) walk3(c, visit);
|
|
6336
|
+
}
|
|
6337
|
+
function sqlalchemyEndpointsFromFile(file, serviceDir) {
|
|
6338
|
+
if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
|
|
6339
|
+
const tree = parseSource5(makePyParser4(), file.content);
|
|
6340
|
+
const out = [];
|
|
6341
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6342
|
+
const push = (name, line) => {
|
|
6343
|
+
if (seen.has(name)) return;
|
|
6344
|
+
seen.add(name);
|
|
6345
|
+
out.push({
|
|
6346
|
+
infraId: infraId8("sql-table", name),
|
|
6347
|
+
name,
|
|
6348
|
+
kind: "sql-table",
|
|
6349
|
+
edgeType: "CALLS",
|
|
6350
|
+
confidenceKind: "verified-call-site",
|
|
6351
|
+
evidence: {
|
|
6352
|
+
file: path32.relative(serviceDir, file.path),
|
|
6353
|
+
line,
|
|
6354
|
+
snippet: snippet(file.content, line)
|
|
6355
|
+
}
|
|
6356
|
+
});
|
|
6357
|
+
};
|
|
6358
|
+
walk3(tree.rootNode, (node) => {
|
|
6359
|
+
if (node.type === "class_definition") {
|
|
6360
|
+
const body = node.childForFieldName("body");
|
|
6361
|
+
const nameNode = node.childForFieldName("name");
|
|
6362
|
+
if (!body || !nameNode) return;
|
|
6363
|
+
const line = node.startPosition.row + 1;
|
|
6364
|
+
const explicit = explicitTablename(body);
|
|
6365
|
+
if (explicit === "computed") return;
|
|
6366
|
+
if (explicit) {
|
|
6367
|
+
push(explicit.name, line);
|
|
6368
|
+
return;
|
|
6369
|
+
}
|
|
6370
|
+
if (extendsFlaskModel(node)) push(flaskSqlalchemyTableName(nameNode.text), line);
|
|
6371
|
+
return;
|
|
6372
|
+
}
|
|
6373
|
+
if (node.type === "call") {
|
|
6374
|
+
const fn = node.childForFieldName("function");
|
|
6375
|
+
const fnText = fn?.text;
|
|
6376
|
+
if (fnText !== "Table" && fnText !== "sa.Table" && fnText !== "sqlalchemy.Table") return;
|
|
6377
|
+
const first = node.childForFieldName("arguments")?.namedChild(0);
|
|
6378
|
+
if (first?.type === "string") {
|
|
6379
|
+
const s = pyStaticStringText2(first);
|
|
6380
|
+
if (s) push(s, node.startPosition.row + 1);
|
|
6381
|
+
}
|
|
6382
|
+
}
|
|
6383
|
+
});
|
|
6384
|
+
return out;
|
|
6385
|
+
}
|
|
6386
|
+
|
|
6242
6387
|
// src/extract/calls/index.ts
|
|
6243
6388
|
function edgeTypeFromEndpoint(ep) {
|
|
6244
6389
|
switch (ep.edgeType) {
|
|
@@ -6271,6 +6416,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
6271
6416
|
endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
|
|
6272
6417
|
endpoints.push(...supabaseEndpointsFromFile(maskedFile, service.dir));
|
|
6273
6418
|
endpoints.push(...mongooseEndpointsFromFile(maskedFile, service.dir));
|
|
6419
|
+
endpoints.push(...sqlalchemyEndpointsFromFile(maskedFile, service.dir));
|
|
6274
6420
|
}
|
|
6275
6421
|
endpoints.push(...await mongooseCrossFileEndpoints(maskedFiles, service.dir));
|
|
6276
6422
|
if (endpoints.length === 0) continue;
|
|
@@ -6343,14 +6489,14 @@ async function addCallEdges(graph, services) {
|
|
|
6343
6489
|
}
|
|
6344
6490
|
|
|
6345
6491
|
// src/extract/infra/docker-compose.ts
|
|
6346
|
-
import
|
|
6492
|
+
import path33 from "path";
|
|
6347
6493
|
import { EdgeType as EdgeType13, Provenance as Provenance13, confidenceForExtracted as confidenceForExtracted11 } from "@neat.is/types";
|
|
6348
6494
|
|
|
6349
6495
|
// src/extract/infra/shared.ts
|
|
6350
|
-
import { NodeType as NodeType13, Provenance as Provenance12, confidenceForExtracted as confidenceForExtracted10, infraId as
|
|
6496
|
+
import { NodeType as NodeType13, Provenance as Provenance12, confidenceForExtracted as confidenceForExtracted10, infraId as infraId9 } from "@neat.is/types";
|
|
6351
6497
|
function makeInfraNode(kind, name, provider = "self", extras) {
|
|
6352
6498
|
return {
|
|
6353
|
-
id:
|
|
6499
|
+
id: infraId9(kind, name),
|
|
6354
6500
|
type: NodeType13.InfraNode,
|
|
6355
6501
|
name,
|
|
6356
6502
|
provider,
|
|
@@ -6413,7 +6559,7 @@ function dependsOnList(value) {
|
|
|
6413
6559
|
}
|
|
6414
6560
|
function serviceNameToServiceNode(name, services) {
|
|
6415
6561
|
for (const s of services) {
|
|
6416
|
-
if (s.node.name === name ||
|
|
6562
|
+
if (s.node.name === name || path33.basename(s.dir) === name) return s.node.id;
|
|
6417
6563
|
}
|
|
6418
6564
|
return null;
|
|
6419
6565
|
}
|
|
@@ -6422,7 +6568,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
6422
6568
|
let edgesAdded = 0;
|
|
6423
6569
|
let composePath = null;
|
|
6424
6570
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
6425
|
-
const abs =
|
|
6571
|
+
const abs = path33.join(scanPath, name);
|
|
6426
6572
|
if (await exists(abs)) {
|
|
6427
6573
|
composePath = abs;
|
|
6428
6574
|
break;
|
|
@@ -6435,13 +6581,13 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
6435
6581
|
} catch (err) {
|
|
6436
6582
|
recordExtractionError(
|
|
6437
6583
|
"infra docker-compose",
|
|
6438
|
-
|
|
6584
|
+
path33.relative(scanPath, composePath),
|
|
6439
6585
|
err
|
|
6440
6586
|
);
|
|
6441
6587
|
return { nodesAdded, edgesAdded };
|
|
6442
6588
|
}
|
|
6443
6589
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
6444
|
-
const evidenceFile =
|
|
6590
|
+
const evidenceFile = path33.relative(scanPath, composePath).split(path33.sep).join("/");
|
|
6445
6591
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
6446
6592
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
6447
6593
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -6482,7 +6628,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
6482
6628
|
}
|
|
6483
6629
|
|
|
6484
6630
|
// src/extract/infra/dockerfile.ts
|
|
6485
|
-
import
|
|
6631
|
+
import path34 from "path";
|
|
6486
6632
|
import { promises as fs16 } from "fs";
|
|
6487
6633
|
import { EdgeType as EdgeType14, Provenance as Provenance14, confidenceForExtracted as confidenceForExtracted12 } from "@neat.is/types";
|
|
6488
6634
|
function readDockerfile(content) {
|
|
@@ -6513,7 +6659,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
6513
6659
|
let nodesAdded = 0;
|
|
6514
6660
|
let edgesAdded = 0;
|
|
6515
6661
|
for (const service of services) {
|
|
6516
|
-
const dockerfilePath =
|
|
6662
|
+
const dockerfilePath = path34.join(service.dir, "Dockerfile");
|
|
6517
6663
|
if (!await exists(dockerfilePath)) continue;
|
|
6518
6664
|
let content;
|
|
6519
6665
|
try {
|
|
@@ -6521,7 +6667,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
6521
6667
|
} catch (err) {
|
|
6522
6668
|
recordExtractionError(
|
|
6523
6669
|
"infra dockerfile",
|
|
6524
|
-
|
|
6670
|
+
path34.relative(scanPath, dockerfilePath),
|
|
6525
6671
|
err
|
|
6526
6672
|
);
|
|
6527
6673
|
continue;
|
|
@@ -6533,8 +6679,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
6533
6679
|
graph.addNode(node.id, node);
|
|
6534
6680
|
nodesAdded++;
|
|
6535
6681
|
}
|
|
6536
|
-
const relDockerfile = toPosix2(
|
|
6537
|
-
const evidenceFile = toPosix2(
|
|
6682
|
+
const relDockerfile = toPosix2(path34.relative(service.dir, dockerfilePath));
|
|
6683
|
+
const evidenceFile = toPosix2(path34.relative(scanPath, dockerfilePath));
|
|
6538
6684
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
6539
6685
|
graph,
|
|
6540
6686
|
service.pkg.name,
|
|
@@ -6586,7 +6732,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
6586
6732
|
|
|
6587
6733
|
// src/extract/infra/terraform.ts
|
|
6588
6734
|
import { promises as fs17 } from "fs";
|
|
6589
|
-
import
|
|
6735
|
+
import path35 from "path";
|
|
6590
6736
|
import { EdgeType as EdgeType15, Provenance as Provenance15, confidenceForExtracted as confidenceForExtracted13 } from "@neat.is/types";
|
|
6591
6737
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
6592
6738
|
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
@@ -6597,11 +6743,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
6597
6743
|
for (const entry of entries) {
|
|
6598
6744
|
if (entry.isDirectory()) {
|
|
6599
6745
|
if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
|
|
6600
|
-
const child =
|
|
6746
|
+
const child = path35.join(start, entry.name);
|
|
6601
6747
|
if (await isPythonVenvDir(child)) continue;
|
|
6602
6748
|
out.push(...await walkTfFiles(child, depth + 1, max));
|
|
6603
6749
|
} else if (entry.isFile() && entry.name.endsWith(".tf")) {
|
|
6604
|
-
out.push(
|
|
6750
|
+
out.push(path35.join(start, entry.name));
|
|
6605
6751
|
}
|
|
6606
6752
|
}
|
|
6607
6753
|
return out;
|
|
@@ -6633,7 +6779,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
6633
6779
|
const files = await walkTfFiles(scanPath);
|
|
6634
6780
|
for (const file of files) {
|
|
6635
6781
|
const content = await fs17.readFile(file, "utf8");
|
|
6636
|
-
const evidenceFile = toPosix2(
|
|
6782
|
+
const evidenceFile = toPosix2(path35.relative(scanPath, file));
|
|
6637
6783
|
const resources = [];
|
|
6638
6784
|
const byKey = /* @__PURE__ */ new Map();
|
|
6639
6785
|
RESOURCE_RE.lastIndex = 0;
|
|
@@ -6690,7 +6836,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
6690
6836
|
|
|
6691
6837
|
// src/extract/infra/k8s.ts
|
|
6692
6838
|
import { promises as fs18 } from "fs";
|
|
6693
|
-
import
|
|
6839
|
+
import path36 from "path";
|
|
6694
6840
|
import { parseAllDocuments as parseAllDocuments2 } from "yaml";
|
|
6695
6841
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
6696
6842
|
Service: "k8s-service",
|
|
@@ -6708,11 +6854,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
|
6708
6854
|
for (const entry of entries) {
|
|
6709
6855
|
if (entry.isDirectory()) {
|
|
6710
6856
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
6711
|
-
const child =
|
|
6857
|
+
const child = path36.join(start, entry.name);
|
|
6712
6858
|
if (await isPythonVenvDir(child)) continue;
|
|
6713
6859
|
out.push(...await walkYamlFiles2(child, depth + 1, max));
|
|
6714
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
6715
|
-
out.push(
|
|
6860
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path36.extname(entry.name))) {
|
|
6861
|
+
out.push(path36.join(start, entry.name));
|
|
6716
6862
|
}
|
|
6717
6863
|
}
|
|
6718
6864
|
return out;
|
|
@@ -6745,13 +6891,13 @@ async function addK8sResources(graph, scanPath) {
|
|
|
6745
6891
|
|
|
6746
6892
|
// src/extract/infra/cloudflare.ts
|
|
6747
6893
|
import { promises as fs19 } from "fs";
|
|
6748
|
-
import
|
|
6894
|
+
import path37 from "path";
|
|
6749
6895
|
import { parse as parseToml2 } from "smol-toml";
|
|
6750
6896
|
import { EdgeType as EdgeType16, Provenance as Provenance16, confidenceForExtracted as confidenceForExtracted14 } from "@neat.is/types";
|
|
6751
6897
|
var WRANGLER_FILENAMES = ["wrangler.toml", "wrangler.jsonc", "wrangler.json"];
|
|
6752
6898
|
async function readWranglerConfig(dir) {
|
|
6753
6899
|
for (const filename of WRANGLER_FILENAMES) {
|
|
6754
|
-
const abs =
|
|
6900
|
+
const abs = path37.join(dir, filename);
|
|
6755
6901
|
if (!await exists(abs)) continue;
|
|
6756
6902
|
const raw = await fs19.readFile(abs, "utf8");
|
|
6757
6903
|
const config = filename === "wrangler.toml" ? parseToml2(raw) : JSON.parse(maskCommentsInSource(raw));
|
|
@@ -6814,11 +6960,11 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
6814
6960
|
try {
|
|
6815
6961
|
read = await readWranglerConfig(service.dir);
|
|
6816
6962
|
} catch (err) {
|
|
6817
|
-
recordExtractionError("infra cloudflare",
|
|
6963
|
+
recordExtractionError("infra cloudflare", path37.relative(scanPath, service.dir), err);
|
|
6818
6964
|
continue;
|
|
6819
6965
|
}
|
|
6820
6966
|
if (!read || !read.config.name) continue;
|
|
6821
|
-
const evidenceFile = toPosix2(
|
|
6967
|
+
const evidenceFile = toPosix2(path37.relative(scanPath, path37.join(service.dir, read.relFile)));
|
|
6822
6968
|
discovered.push({ service, config: read.config, relFile: read.relFile, raw: read.raw, evidenceFile });
|
|
6823
6969
|
}
|
|
6824
6970
|
for (const worker of discovered) {
|
|
@@ -6830,7 +6976,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
6830
6976
|
}
|
|
6831
6977
|
let anchorId = service.node.id;
|
|
6832
6978
|
if (config.main) {
|
|
6833
|
-
const entryRelPath = toPosix2(
|
|
6979
|
+
const entryRelPath = toPosix2(path37.normalize(config.main));
|
|
6834
6980
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
6835
6981
|
graph,
|
|
6836
6982
|
service.pkg.name,
|
|
@@ -6977,12 +7123,12 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
6977
7123
|
|
|
6978
7124
|
// src/extract/infra/vercel.ts
|
|
6979
7125
|
import { promises as fs20 } from "fs";
|
|
6980
|
-
import
|
|
7126
|
+
import path38 from "path";
|
|
6981
7127
|
import { EdgeType as EdgeType17 } from "@neat.is/types";
|
|
6982
7128
|
var VERCEL_CONFIG_FILENAMES = ["vercel.json", "vercel.jsonc"];
|
|
6983
7129
|
async function readVercelConfig(dir) {
|
|
6984
7130
|
for (const filename of VERCEL_CONFIG_FILENAMES) {
|
|
6985
|
-
const abs =
|
|
7131
|
+
const abs = path38.join(dir, filename);
|
|
6986
7132
|
if (!await exists(abs)) continue;
|
|
6987
7133
|
const raw = await fs20.readFile(abs, "utf8");
|
|
6988
7134
|
const config = JSON.parse(maskCommentsInSource(raw));
|
|
@@ -6991,7 +7137,7 @@ async function readVercelConfig(dir) {
|
|
|
6991
7137
|
return null;
|
|
6992
7138
|
}
|
|
6993
7139
|
async function readLinkedProjectName(dir) {
|
|
6994
|
-
const abs =
|
|
7140
|
+
const abs = path38.join(dir, ".vercel", "project.json");
|
|
6995
7141
|
if (!await exists(abs)) return void 0;
|
|
6996
7142
|
const parsed = JSON.parse(await fs20.readFile(abs, "utf8"));
|
|
6997
7143
|
return typeof parsed.projectName === "string" ? parsed.projectName : void 0;
|
|
@@ -7009,7 +7155,7 @@ async function addVercelServices(graph, services, scanPath) {
|
|
|
7009
7155
|
read = await readVercelConfig(service.dir);
|
|
7010
7156
|
projectName = await readLinkedProjectName(service.dir);
|
|
7011
7157
|
} catch (err) {
|
|
7012
|
-
recordExtractionError("infra vercel",
|
|
7158
|
+
recordExtractionError("infra vercel", path38.relative(scanPath, service.dir), err);
|
|
7013
7159
|
continue;
|
|
7014
7160
|
}
|
|
7015
7161
|
if (!read && !projectName) continue;
|
|
@@ -7025,7 +7171,7 @@ async function addVercelServices(graph, services, scanPath) {
|
|
|
7025
7171
|
const anchorId = service.node.id;
|
|
7026
7172
|
if (!read) continue;
|
|
7027
7173
|
const { config, relFile, raw } = read;
|
|
7028
|
-
const evidenceFile = toPosix2(
|
|
7174
|
+
const evidenceFile = toPosix2(path38.relative(scanPath, path38.join(service.dir, relFile)));
|
|
7029
7175
|
const add = (edgeType, kind, name) => {
|
|
7030
7176
|
if (!name) return;
|
|
7031
7177
|
const result = emitPlatformResourceEdge(
|
|
@@ -7054,13 +7200,13 @@ async function addVercelServices(graph, services, scanPath) {
|
|
|
7054
7200
|
|
|
7055
7201
|
// src/extract/infra/railway.ts
|
|
7056
7202
|
import { promises as fs21 } from "fs";
|
|
7057
|
-
import
|
|
7203
|
+
import path39 from "path";
|
|
7058
7204
|
import { parse as parseToml3 } from "smol-toml";
|
|
7059
7205
|
import { EdgeType as EdgeType18 } from "@neat.is/types";
|
|
7060
7206
|
var RAILWAY_FILENAMES = ["railway.toml", "railway.json", "railway.jsonc"];
|
|
7061
7207
|
async function readRailwayConfig(dir) {
|
|
7062
7208
|
for (const filename of RAILWAY_FILENAMES) {
|
|
7063
|
-
const abs =
|
|
7209
|
+
const abs = path39.join(dir, filename);
|
|
7064
7210
|
if (!await exists(abs)) continue;
|
|
7065
7211
|
const raw = await fs21.readFile(abs, "utf8");
|
|
7066
7212
|
const config = filename === "railway.toml" ? parseToml3(raw) : JSON.parse(maskCommentsInSource(raw));
|
|
@@ -7076,7 +7222,7 @@ async function addRailwayServices(graph, services, scanPath) {
|
|
|
7076
7222
|
try {
|
|
7077
7223
|
read = await readRailwayConfig(service.dir);
|
|
7078
7224
|
} catch (err) {
|
|
7079
|
-
recordExtractionError("infra railway",
|
|
7225
|
+
recordExtractionError("infra railway", path39.relative(scanPath, service.dir), err);
|
|
7080
7226
|
continue;
|
|
7081
7227
|
}
|
|
7082
7228
|
if (!read) continue;
|
|
@@ -7086,7 +7232,7 @@ async function addRailwayServices(graph, services, scanPath) {
|
|
|
7086
7232
|
}
|
|
7087
7233
|
const anchorId = service.node.id;
|
|
7088
7234
|
const { config, relFile, raw } = read;
|
|
7089
|
-
const evidenceFile = toPosix2(
|
|
7235
|
+
const evidenceFile = toPosix2(path39.relative(scanPath, path39.join(service.dir, relFile)));
|
|
7090
7236
|
const add = (edgeType, kind, name) => {
|
|
7091
7237
|
if (!name) return;
|
|
7092
7238
|
const result = emitPlatformResourceEdge(
|
|
@@ -7111,12 +7257,12 @@ async function addRailwayServices(graph, services, scanPath) {
|
|
|
7111
7257
|
|
|
7112
7258
|
// src/extract/infra/supabase.ts
|
|
7113
7259
|
import { promises as fs22 } from "fs";
|
|
7114
|
-
import
|
|
7260
|
+
import path40 from "path";
|
|
7115
7261
|
import { parse as parseToml4 } from "smol-toml";
|
|
7116
7262
|
import { EdgeType as EdgeType19 } from "@neat.is/types";
|
|
7117
7263
|
async function readSupabaseConfig(dir) {
|
|
7118
|
-
const relFile =
|
|
7119
|
-
const abs =
|
|
7264
|
+
const relFile = path40.join("supabase", "config.toml");
|
|
7265
|
+
const abs = path40.join(dir, relFile);
|
|
7120
7266
|
if (!await exists(abs)) return null;
|
|
7121
7267
|
const raw = await fs22.readFile(abs, "utf8");
|
|
7122
7268
|
const config = parseToml4(raw);
|
|
@@ -7130,7 +7276,7 @@ async function addSupabaseProjects(graph, services, scanPath) {
|
|
|
7130
7276
|
try {
|
|
7131
7277
|
read = await readSupabaseConfig(service.dir);
|
|
7132
7278
|
} catch (err) {
|
|
7133
|
-
recordExtractionError("infra supabase",
|
|
7279
|
+
recordExtractionError("infra supabase", path40.relative(scanPath, service.dir), err);
|
|
7134
7280
|
continue;
|
|
7135
7281
|
}
|
|
7136
7282
|
if (!read) continue;
|
|
@@ -7145,7 +7291,7 @@ async function addSupabaseProjects(graph, services, scanPath) {
|
|
|
7145
7291
|
});
|
|
7146
7292
|
}
|
|
7147
7293
|
const anchorId = service.node.id;
|
|
7148
|
-
const evidenceFile = toPosix2(
|
|
7294
|
+
const evidenceFile = toPosix2(path40.relative(scanPath, path40.join(service.dir, relFile)));
|
|
7149
7295
|
const add = (edgeType, kind, name) => {
|
|
7150
7296
|
if (!name) return;
|
|
7151
7297
|
const result = emitPlatformResourceEdge(
|
|
@@ -7186,11 +7332,11 @@ async function addInfra(graph, scanPath, services) {
|
|
|
7186
7332
|
}
|
|
7187
7333
|
|
|
7188
7334
|
// src/extract/index.ts
|
|
7189
|
-
import
|
|
7335
|
+
import path42 from "path";
|
|
7190
7336
|
|
|
7191
7337
|
// src/extract/retire.ts
|
|
7192
7338
|
import { existsSync as existsSync2 } from "fs";
|
|
7193
|
-
import
|
|
7339
|
+
import path41 from "path";
|
|
7194
7340
|
import { NodeType as NodeType14, Provenance as Provenance17 } from "@neat.is/types";
|
|
7195
7341
|
function dropOrphanedFileNodes(graph) {
|
|
7196
7342
|
const orphans = [];
|
|
@@ -7224,11 +7370,11 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
7224
7370
|
if (edge.provenance !== Provenance17.EXTRACTED) return;
|
|
7225
7371
|
const evidenceFile = edge.evidence?.file;
|
|
7226
7372
|
if (!evidenceFile) return;
|
|
7227
|
-
if (
|
|
7373
|
+
if (path41.isAbsolute(evidenceFile)) {
|
|
7228
7374
|
if (!existsSync2(evidenceFile)) toDrop.push(id);
|
|
7229
7375
|
return;
|
|
7230
7376
|
}
|
|
7231
|
-
const found = bases.some((base) => existsSync2(
|
|
7377
|
+
const found = bases.some((base) => existsSync2(path41.join(base, evidenceFile)));
|
|
7232
7378
|
if (!found) toDrop.push(id);
|
|
7233
7379
|
});
|
|
7234
7380
|
for (const id of toDrop) graph.dropEdge(id);
|
|
@@ -7270,7 +7416,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
7270
7416
|
}
|
|
7271
7417
|
const droppedEntries = drainDroppedExtracted();
|
|
7272
7418
|
if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
|
|
7273
|
-
const rejectedPath =
|
|
7419
|
+
const rejectedPath = path42.join(path42.dirname(opts.errorsPath), "rejected.ndjson");
|
|
7274
7420
|
try {
|
|
7275
7421
|
await writeRejectedExtracted(droppedEntries, rejectedPath);
|
|
7276
7422
|
} catch (err) {
|
|
@@ -7588,7 +7734,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
7588
7734
|
|
|
7589
7735
|
// src/persist.ts
|
|
7590
7736
|
import { promises as fs23 } from "fs";
|
|
7591
|
-
import
|
|
7737
|
+
import path43 from "path";
|
|
7592
7738
|
import { Provenance as Provenance19, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
|
|
7593
7739
|
var SCHEMA_VERSION = 4;
|
|
7594
7740
|
function migrateV1ToV2(payload) {
|
|
@@ -7625,7 +7771,7 @@ function migrateV2ToV3(payload) {
|
|
|
7625
7771
|
return { ...payload, schemaVersion: 3 };
|
|
7626
7772
|
}
|
|
7627
7773
|
async function ensureDir(filePath) {
|
|
7628
|
-
await fs23.mkdir(
|
|
7774
|
+
await fs23.mkdir(path43.dirname(filePath), { recursive: true });
|
|
7629
7775
|
}
|
|
7630
7776
|
async function saveGraphToDisk(graph, outPath) {
|
|
7631
7777
|
await ensureDir(outPath);
|
|
@@ -7781,23 +7927,23 @@ function canonicalJson(value) {
|
|
|
7781
7927
|
}
|
|
7782
7928
|
|
|
7783
7929
|
// src/projects.ts
|
|
7784
|
-
import
|
|
7930
|
+
import path44 from "path";
|
|
7785
7931
|
function pathsForProject(project, baseDir) {
|
|
7786
7932
|
if (project === DEFAULT_PROJECT) {
|
|
7787
7933
|
return {
|
|
7788
|
-
snapshotPath:
|
|
7789
|
-
errorsPath:
|
|
7790
|
-
staleEventsPath:
|
|
7791
|
-
embeddingsCachePath:
|
|
7792
|
-
policyViolationsPath:
|
|
7934
|
+
snapshotPath: path44.join(baseDir, "graph.json"),
|
|
7935
|
+
errorsPath: path44.join(baseDir, "errors.ndjson"),
|
|
7936
|
+
staleEventsPath: path44.join(baseDir, "stale-events.ndjson"),
|
|
7937
|
+
embeddingsCachePath: path44.join(baseDir, "embeddings.json"),
|
|
7938
|
+
policyViolationsPath: path44.join(baseDir, "policy-violations.ndjson")
|
|
7793
7939
|
};
|
|
7794
7940
|
}
|
|
7795
7941
|
return {
|
|
7796
|
-
snapshotPath:
|
|
7797
|
-
errorsPath:
|
|
7798
|
-
staleEventsPath:
|
|
7799
|
-
embeddingsCachePath:
|
|
7800
|
-
policyViolationsPath:
|
|
7942
|
+
snapshotPath: path44.join(baseDir, `${project}.json`),
|
|
7943
|
+
errorsPath: path44.join(baseDir, `errors.${project}.ndjson`),
|
|
7944
|
+
staleEventsPath: path44.join(baseDir, `stale-events.${project}.ndjson`),
|
|
7945
|
+
embeddingsCachePath: path44.join(baseDir, `embeddings.${project}.json`),
|
|
7946
|
+
policyViolationsPath: path44.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
7801
7947
|
};
|
|
7802
7948
|
}
|
|
7803
7949
|
var Projects = class {
|
|
@@ -7838,7 +7984,7 @@ function parseExtraProjects(raw) {
|
|
|
7838
7984
|
// src/registry.ts
|
|
7839
7985
|
import { promises as fs25 } from "fs";
|
|
7840
7986
|
import os2 from "os";
|
|
7841
|
-
import
|
|
7987
|
+
import path45 from "path";
|
|
7842
7988
|
import {
|
|
7843
7989
|
RegistryFileSchema
|
|
7844
7990
|
} from "@neat.is/types";
|
|
@@ -7846,20 +7992,20 @@ var LOCK_TIMEOUT_MS = 5e3;
|
|
|
7846
7992
|
var LOCK_RETRY_MS = 50;
|
|
7847
7993
|
function neatHome() {
|
|
7848
7994
|
const override = process.env.NEAT_HOME;
|
|
7849
|
-
if (override && override.length > 0) return
|
|
7850
|
-
return
|
|
7995
|
+
if (override && override.length > 0) return path45.resolve(override);
|
|
7996
|
+
return path45.join(os2.homedir(), ".neat");
|
|
7851
7997
|
}
|
|
7852
7998
|
function registryPath() {
|
|
7853
|
-
return
|
|
7999
|
+
return path45.join(neatHome(), "projects.json");
|
|
7854
8000
|
}
|
|
7855
8001
|
function registryLockPath() {
|
|
7856
|
-
return
|
|
8002
|
+
return path45.join(neatHome(), "projects.json.lock");
|
|
7857
8003
|
}
|
|
7858
8004
|
function daemonPidPath() {
|
|
7859
|
-
return
|
|
8005
|
+
return path45.join(neatHome(), "neatd.pid");
|
|
7860
8006
|
}
|
|
7861
8007
|
function daemonsDir() {
|
|
7862
|
-
return
|
|
8008
|
+
return path45.join(neatHome(), "daemons");
|
|
7863
8009
|
}
|
|
7864
8010
|
function isFiniteInt(v) {
|
|
7865
8011
|
return typeof v === "number" && Number.isFinite(v);
|
|
@@ -7900,7 +8046,7 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
|
|
|
7900
8046
|
const out = [];
|
|
7901
8047
|
for (const name of names) {
|
|
7902
8048
|
if (!name.endsWith(".json")) continue;
|
|
7903
|
-
const file =
|
|
8049
|
+
const file = path45.join(dir, name);
|
|
7904
8050
|
let raw;
|
|
7905
8051
|
try {
|
|
7906
8052
|
raw = await fs25.readFile(file, "utf8");
|
|
@@ -8021,7 +8167,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
|
|
|
8021
8167
|
}
|
|
8022
8168
|
}
|
|
8023
8169
|
async function normalizeProjectPath(input) {
|
|
8024
|
-
const resolved =
|
|
8170
|
+
const resolved = path45.resolve(input);
|
|
8025
8171
|
try {
|
|
8026
8172
|
return await fs25.realpath(resolved);
|
|
8027
8173
|
} catch {
|
|
@@ -8029,7 +8175,7 @@ async function normalizeProjectPath(input) {
|
|
|
8029
8175
|
}
|
|
8030
8176
|
}
|
|
8031
8177
|
async function writeAtomically(target, contents) {
|
|
8032
|
-
await fs25.mkdir(
|
|
8178
|
+
await fs25.mkdir(path45.dirname(target), { recursive: true });
|
|
8033
8179
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
8034
8180
|
const fd = await fs25.open(tmp, "w");
|
|
8035
8181
|
try {
|
|
@@ -8042,7 +8188,7 @@ async function writeAtomically(target, contents) {
|
|
|
8042
8188
|
}
|
|
8043
8189
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
|
|
8044
8190
|
const deadline = Date.now() + timeoutMs;
|
|
8045
|
-
await fs25.mkdir(
|
|
8191
|
+
await fs25.mkdir(path45.dirname(lockPath), { recursive: true });
|
|
8046
8192
|
let probedHolder = false;
|
|
8047
8193
|
while (true) {
|
|
8048
8194
|
try {
|
|
@@ -8238,13 +8384,13 @@ import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } f
|
|
|
8238
8384
|
|
|
8239
8385
|
// src/extend/index.ts
|
|
8240
8386
|
import { promises as fs27 } from "fs";
|
|
8241
|
-
import
|
|
8387
|
+
import path47 from "path";
|
|
8242
8388
|
import os3 from "os";
|
|
8243
8389
|
import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
|
|
8244
8390
|
|
|
8245
8391
|
// src/installers/package-manager.ts
|
|
8246
8392
|
import { promises as fs26 } from "fs";
|
|
8247
|
-
import
|
|
8393
|
+
import path46 from "path";
|
|
8248
8394
|
import { spawn } from "child_process";
|
|
8249
8395
|
var LOCKFILE_PRIORITY = [
|
|
8250
8396
|
{ lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
|
|
@@ -8266,22 +8412,22 @@ async function exists2(p) {
|
|
|
8266
8412
|
}
|
|
8267
8413
|
}
|
|
8268
8414
|
async function detectPackageManager(serviceDir) {
|
|
8269
|
-
let dir =
|
|
8415
|
+
let dir = path46.resolve(serviceDir);
|
|
8270
8416
|
const stops = /* @__PURE__ */ new Set();
|
|
8271
8417
|
for (let i = 0; i < 64; i++) {
|
|
8272
8418
|
if (stops.has(dir)) break;
|
|
8273
8419
|
stops.add(dir);
|
|
8274
8420
|
for (const candidate of LOCKFILE_PRIORITY) {
|
|
8275
|
-
const lockPath =
|
|
8421
|
+
const lockPath = path46.join(dir, candidate.lockfile);
|
|
8276
8422
|
if (await exists2(lockPath)) {
|
|
8277
8423
|
return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
|
|
8278
8424
|
}
|
|
8279
8425
|
}
|
|
8280
|
-
const parent =
|
|
8426
|
+
const parent = path46.dirname(dir);
|
|
8281
8427
|
if (parent === dir) break;
|
|
8282
8428
|
dir = parent;
|
|
8283
8429
|
}
|
|
8284
|
-
return { pm: "npm", cwd:
|
|
8430
|
+
return { pm: "npm", cwd: path46.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
|
|
8285
8431
|
}
|
|
8286
8432
|
async function runPackageManagerInstall(cmd) {
|
|
8287
8433
|
return new Promise((resolve) => {
|
|
@@ -8330,7 +8476,7 @@ async function fileExists2(p) {
|
|
|
8330
8476
|
}
|
|
8331
8477
|
}
|
|
8332
8478
|
async function readPackageJson(scanPath) {
|
|
8333
|
-
const pkgPath =
|
|
8479
|
+
const pkgPath = path47.join(scanPath, "package.json");
|
|
8334
8480
|
const raw = await fs27.readFile(pkgPath, "utf8");
|
|
8335
8481
|
return JSON.parse(raw);
|
|
8336
8482
|
}
|
|
@@ -8344,27 +8490,27 @@ var HOOK_WALK_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
8344
8490
|
]);
|
|
8345
8491
|
async function findHookFiles(scanPath) {
|
|
8346
8492
|
const found = [];
|
|
8347
|
-
const
|
|
8493
|
+
const walk4 = async (dir) => {
|
|
8348
8494
|
const entries = await fs27.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
8349
8495
|
for (const entry of entries) {
|
|
8350
8496
|
if (entry.isDirectory()) {
|
|
8351
8497
|
if (entry.name.startsWith(".") || HOOK_WALK_SKIP_DIRS.has(entry.name)) continue;
|
|
8352
|
-
await
|
|
8498
|
+
await walk4(path47.join(dir, entry.name));
|
|
8353
8499
|
} else if (entry.isFile()) {
|
|
8354
8500
|
if ((entry.name.startsWith("instrumentation") || entry.name.startsWith("otel-init")) && /\.(ts|js|cjs|mjs)$/.test(entry.name)) {
|
|
8355
|
-
const rel =
|
|
8356
|
-
found.push(rel.split(
|
|
8501
|
+
const rel = path47.relative(scanPath, path47.join(dir, entry.name));
|
|
8502
|
+
found.push(rel.split(path47.sep).join("/"));
|
|
8357
8503
|
}
|
|
8358
8504
|
}
|
|
8359
8505
|
}
|
|
8360
8506
|
};
|
|
8361
|
-
await
|
|
8507
|
+
await walk4(scanPath);
|
|
8362
8508
|
return found.sort();
|
|
8363
8509
|
}
|
|
8364
8510
|
async function pickPrimaryHookFile(scanPath, hookFiles, snippet2) {
|
|
8365
8511
|
let fallback = null;
|
|
8366
8512
|
for (const file of hookFiles) {
|
|
8367
|
-
const content = await fs27.readFile(
|
|
8513
|
+
const content = await fs27.readFile(path47.join(scanPath, file), "utf8");
|
|
8368
8514
|
const patched = splicedContent(content, snippet2);
|
|
8369
8515
|
if (patched !== null) return { file, content, patched };
|
|
8370
8516
|
if (fallback === null) fallback = { file, content };
|
|
@@ -8372,11 +8518,11 @@ async function pickPrimaryHookFile(scanPath, hookFiles, snippet2) {
|
|
|
8372
8518
|
return { file: fallback.file, content: fallback.content, patched: null };
|
|
8373
8519
|
}
|
|
8374
8520
|
function extendLogPath() {
|
|
8375
|
-
return process.env.NEAT_EXTEND_LOG ??
|
|
8521
|
+
return process.env.NEAT_EXTEND_LOG ?? path47.join(os3.homedir(), ".neat", "extend-log.ndjson");
|
|
8376
8522
|
}
|
|
8377
8523
|
async function appendExtendLog(entry) {
|
|
8378
8524
|
const logPath = extendLogPath();
|
|
8379
|
-
await fs27.mkdir(
|
|
8525
|
+
await fs27.mkdir(path47.dirname(logPath), { recursive: true });
|
|
8380
8526
|
await fs27.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
|
|
8381
8527
|
}
|
|
8382
8528
|
function splicedContent(fileContent, snippet2) {
|
|
@@ -8435,7 +8581,7 @@ function lookupInstrumentation(library, installedVersion) {
|
|
|
8435
8581
|
}
|
|
8436
8582
|
async function describeProjectInstrumentation(ctx) {
|
|
8437
8583
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
8438
|
-
const envNeat = await fileExists2(
|
|
8584
|
+
const envNeat = await fileExists2(path47.join(ctx.scanPath, ".env.neat"));
|
|
8439
8585
|
const registryInstrPackages = new Set(
|
|
8440
8586
|
registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
|
|
8441
8587
|
);
|
|
@@ -8457,7 +8603,7 @@ async function applyExtension(ctx, args, options) {
|
|
|
8457
8603
|
);
|
|
8458
8604
|
}
|
|
8459
8605
|
for (const file of hookFiles) {
|
|
8460
|
-
const content = await fs27.readFile(
|
|
8606
|
+
const content = await fs27.readFile(path47.join(ctx.scanPath, file), "utf8");
|
|
8461
8607
|
if (content.includes(args.registration_snippet)) {
|
|
8462
8608
|
return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
|
|
8463
8609
|
}
|
|
@@ -8469,10 +8615,10 @@ async function applyExtension(ctx, args, options) {
|
|
|
8469
8615
|
);
|
|
8470
8616
|
}
|
|
8471
8617
|
const primaryFile = primary.file;
|
|
8472
|
-
const primaryPath =
|
|
8618
|
+
const primaryPath = path47.join(ctx.scanPath, primaryFile);
|
|
8473
8619
|
const filesTouched = [];
|
|
8474
8620
|
const depsAdded = [];
|
|
8475
|
-
const pkgPath =
|
|
8621
|
+
const pkgPath = path47.join(ctx.scanPath, "package.json");
|
|
8476
8622
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
8477
8623
|
if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
|
|
8478
8624
|
pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
|
|
@@ -8511,7 +8657,7 @@ async function dryRunExtension(ctx, args) {
|
|
|
8511
8657
|
};
|
|
8512
8658
|
}
|
|
8513
8659
|
for (const file of hookFiles) {
|
|
8514
|
-
const content = await fs27.readFile(
|
|
8660
|
+
const content = await fs27.readFile(path47.join(ctx.scanPath, file), "utf8");
|
|
8515
8661
|
if (content.includes(args.registration_snippet)) {
|
|
8516
8662
|
return {
|
|
8517
8663
|
library: args.library,
|
|
@@ -8552,7 +8698,7 @@ async function rollbackExtension(ctx, args) {
|
|
|
8552
8698
|
if (!match) {
|
|
8553
8699
|
return { undone: false, message: "no apply found for library" };
|
|
8554
8700
|
}
|
|
8555
|
-
const pkgPath =
|
|
8701
|
+
const pkgPath = path47.join(ctx.scanPath, "package.json");
|
|
8556
8702
|
if (await fileExists2(pkgPath)) {
|
|
8557
8703
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
8558
8704
|
if (pkg.dependencies?.[match.instrumentation_package]) {
|
|
@@ -8563,7 +8709,7 @@ async function rollbackExtension(ctx, args) {
|
|
|
8563
8709
|
}
|
|
8564
8710
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
8565
8711
|
for (const file of hookFiles) {
|
|
8566
|
-
const filePath =
|
|
8712
|
+
const filePath = path47.join(ctx.scanPath, file);
|
|
8567
8713
|
const content = await fs27.readFile(filePath, "utf8");
|
|
8568
8714
|
if (content.includes(match.registration_snippet)) {
|
|
8569
8715
|
const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
|
|
@@ -8678,7 +8824,7 @@ data: ${JSON.stringify(envelope.payload)}
|
|
|
8678
8824
|
|
|
8679
8825
|
// src/connectors-config.ts
|
|
8680
8826
|
import os4 from "os";
|
|
8681
|
-
import
|
|
8827
|
+
import path48 from "path";
|
|
8682
8828
|
import { promises as fs28 } from "fs";
|
|
8683
8829
|
var CONNECTORS_CONFIG_VERSION = 1;
|
|
8684
8830
|
var EnvRefUnsetError = class extends Error {
|
|
@@ -8693,11 +8839,11 @@ var EnvRefUnsetError = class extends Error {
|
|
|
8693
8839
|
};
|
|
8694
8840
|
function neatHome2() {
|
|
8695
8841
|
const override = process.env.NEAT_HOME;
|
|
8696
|
-
if (override && override.length > 0) return
|
|
8697
|
-
return
|
|
8842
|
+
if (override && override.length > 0) return path48.resolve(override);
|
|
8843
|
+
return path48.join(os4.homedir(), ".neat");
|
|
8698
8844
|
}
|
|
8699
8845
|
function connectorsConfigPath(home = neatHome2()) {
|
|
8700
|
-
return
|
|
8846
|
+
return path48.join(home, "connectors.json");
|
|
8701
8847
|
}
|
|
8702
8848
|
var MODE_MASK_LOOSER_THAN_0600 = 63;
|
|
8703
8849
|
async function warnIfModeLooserThan0600(file) {
|
|
@@ -8828,7 +8974,7 @@ function connectorMatchesProject(entry, project) {
|
|
|
8828
8974
|
var CONNECTORS_LOCK_TIMEOUT_MS = 5e3;
|
|
8829
8975
|
var CONNECTORS_LOCK_RETRY_MS = 50;
|
|
8830
8976
|
function connectorsConfigLockPath(home = neatHome2()) {
|
|
8831
|
-
return
|
|
8977
|
+
return path48.join(home, "connectors.json.lock");
|
|
8832
8978
|
}
|
|
8833
8979
|
function isEnvRef(value) {
|
|
8834
8980
|
return value.length > 1 && value.startsWith("$");
|
|
@@ -8841,7 +8987,7 @@ function redactCredentialRef(ref) {
|
|
|
8841
8987
|
return out;
|
|
8842
8988
|
}
|
|
8843
8989
|
async function writeConfigAtomically0600(file, contents) {
|
|
8844
|
-
await fs28.mkdir(
|
|
8990
|
+
await fs28.mkdir(path48.dirname(file), { recursive: true });
|
|
8845
8991
|
const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
8846
8992
|
const fd = await fs28.open(tmp, "w", 384);
|
|
8847
8993
|
try {
|
|
@@ -8855,7 +9001,7 @@ async function writeConfigAtomically0600(file, contents) {
|
|
|
8855
9001
|
}
|
|
8856
9002
|
async function acquireConnectorsLock(lockPath, timeoutMs = CONNECTORS_LOCK_TIMEOUT_MS) {
|
|
8857
9003
|
const deadline = Date.now() + timeoutMs;
|
|
8858
|
-
await fs28.mkdir(
|
|
9004
|
+
await fs28.mkdir(path48.dirname(lockPath), { recursive: true });
|
|
8859
9005
|
for (; ; ) {
|
|
8860
9006
|
try {
|
|
8861
9007
|
const fd = await fs28.open(lockPath, "wx");
|
|
@@ -9849,4 +9995,4 @@ export {
|
|
|
9849
9995
|
recordConnectorPoll,
|
|
9850
9996
|
buildApi
|
|
9851
9997
|
};
|
|
9852
|
-
//# sourceMappingURL=chunk-
|
|
9998
|
+
//# sourceMappingURL=chunk-ZZ3VUCWL.js.map
|