@neat.is/core 0.6.0 → 0.6.2-dev.20260722
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-5PVQJLPR.js → chunk-64JJOXES.js} +3 -3
- package/dist/{chunk-MVINCLQM.js → chunk-OY7FGJAX.js} +2 -2
- package/dist/{chunk-CS4GHQO3.js → chunk-ZZ3VUCWL.js} +1581 -1329
- package/dist/chunk-ZZ3VUCWL.js.map +1 -0
- package/dist/cli.cjs +730 -468
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +4 -4
- package/dist/index.cjs +579 -317
- 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 +588 -326
- 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 +2683 -2417
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-BZ3AJVAC.js.map +0 -1
- package/dist/chunk-CS4GHQO3.js.map +0 -1
- /package/dist/{chunk-5PVQJLPR.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,
|
|
@@ -2351,7 +1867,7 @@ async function handleSpan(ctx, span) {
|
|
|
2351
1867
|
} : void 0;
|
|
2352
1868
|
upsertObservedEdge(
|
|
2353
1869
|
ctx.graph,
|
|
2354
|
-
|
|
1870
|
+
EdgeType2.CALLS,
|
|
2355
1871
|
fallbackSource,
|
|
2356
1872
|
sourceId,
|
|
2357
1873
|
ts,
|
|
@@ -2361,286 +1877,801 @@ async function handleSpan(ctx, span) {
|
|
|
2361
1877
|
}
|
|
2362
1878
|
}
|
|
2363
1879
|
}
|
|
2364
|
-
if (span.statusCode === 2) {
|
|
2365
|
-
stitchTrace(ctx.graph, sourceId, ts);
|
|
2366
|
-
if (ctx.writeErrorEventInline !== false) {
|
|
2367
|
-
const attrs = sanitizeAttributes(span.attributes);
|
|
2368
|
-
const ev = {
|
|
2369
|
-
id: `${span.traceId}:${span.spanId}`,
|
|
2370
|
-
timestamp: ts,
|
|
2371
|
-
service: span.service,
|
|
2372
|
-
traceId: span.traceId,
|
|
2373
|
-
spanId: span.spanId,
|
|
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);
|
|
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);
|
|
2381
2206
|
}
|
|
2382
2207
|
}
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
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);
|
|
2391
2218
|
}
|
|
2392
2219
|
}
|
|
2393
|
-
|
|
2394
|
-
if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
|
|
2220
|
+
return best;
|
|
2395
2221
|
}
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
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] };
|
|
2404
2282
|
}
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
for (const { frontierId: frontierId2, serviceId: serviceId5 } of toPromote) {
|
|
2417
|
-
if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
|
|
2418
|
-
const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
|
|
2419
|
-
if (!gate.allowed) {
|
|
2420
|
-
continue;
|
|
2421
|
-
}
|
|
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);
|
|
2422
2294
|
}
|
|
2423
|
-
rewireFrontierEdges(graph, frontierId2, serviceId5);
|
|
2424
|
-
graph.dropNode(frontierId2);
|
|
2425
|
-
promoted++;
|
|
2426
2295
|
}
|
|
2427
|
-
|
|
2296
|
+
step(start, [start], []);
|
|
2297
|
+
return best;
|
|
2428
2298
|
}
|
|
2429
|
-
function
|
|
2430
|
-
const
|
|
2431
|
-
const
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
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
|
+
}
|
|
2326
|
+
}
|
|
2435
2327
|
}
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
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
|
+
};
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2439
2364
|
}
|
|
2365
|
+
return null;
|
|
2440
2366
|
}
|
|
2441
|
-
function
|
|
2442
|
-
graph.
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
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
|
+
}
|
|
2453
2395
|
}
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
};
|
|
2460
|
-
graph.addEdgeWithKey(newId, newSource, newTarget, rebuilt);
|
|
2396
|
+
if (origin.type === NodeType3.ServiceNode) {
|
|
2397
|
+
const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
|
|
2398
|
+
if (crossService) return crossService;
|
|
2399
|
+
}
|
|
2400
|
+
return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
|
|
2461
2401
|
}
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
return new Date(a).getTime() >= new Date(b).getTime() ? a : b;
|
|
2402
|
+
var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
|
|
2403
|
+
function incidentMatchesNode(ev, nodeId) {
|
|
2404
|
+
return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
|
|
2466
2405
|
}
|
|
2467
|
-
function
|
|
2468
|
-
|
|
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 } : {}
|
|
2430
|
+
};
|
|
2469
2431
|
}
|
|
2470
|
-
|
|
2471
|
-
const
|
|
2472
|
-
|
|
2473
|
-
const
|
|
2474
|
-
const
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
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
|
-
}
|
|
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 } : {}
|
|
2504
2444
|
});
|
|
2505
|
-
if (options.staleEventsPath && events.length > 0) {
|
|
2506
|
-
await appendStaleEvents(options.staleEventsPath, events);
|
|
2507
|
-
}
|
|
2508
|
-
return { count: events.length, events };
|
|
2509
2445
|
}
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
2513
|
-
await fs3.appendFile(staleEventsPath, lines, "utf8");
|
|
2446
|
+
function isFailingCallEdge(e) {
|
|
2447
|
+
return e.type === EdgeType3.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
2514
2448
|
}
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
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);
|
|
2522
2456
|
}
|
|
2457
|
+
return ids;
|
|
2523
2458
|
}
|
|
2524
|
-
function
|
|
2525
|
-
|
|
2526
|
-
const
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
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];
|
|
2465
|
+
}
|
|
2466
|
+
return id < curId;
|
|
2467
|
+
}
|
|
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;
|
|
2557
2496
|
}
|
|
2497
|
+
if (edges.length === 0) return null;
|
|
2498
|
+
return { path: path49, edges, culprit: current };
|
|
2558
2499
|
}
|
|
2559
|
-
function
|
|
2560
|
-
|
|
2561
|
-
if (
|
|
2562
|
-
|
|
2563
|
-
const
|
|
2564
|
-
|
|
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
|
+
});
|
|
2565
2536
|
}
|
|
2566
|
-
function
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
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
|
+
});
|
|
2574
2555
|
}
|
|
2575
|
-
if (
|
|
2576
|
-
|
|
2577
|
-
|
|
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
|
+
}
|
|
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
|
+
});
|
|
2577
|
+
}
|
|
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) {
|
|
@@ -4653,6 +4684,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
4653
4684
|
import path22 from "path";
|
|
4654
4685
|
import Parser2 from "tree-sitter";
|
|
4655
4686
|
import JavaScript2 from "tree-sitter-javascript";
|
|
4687
|
+
import Python2 from "tree-sitter-python";
|
|
4656
4688
|
import {
|
|
4657
4689
|
EdgeType as EdgeType8,
|
|
4658
4690
|
NodeType as NodeType9,
|
|
@@ -4672,6 +4704,11 @@ function makeJsParser2() {
|
|
|
4672
4704
|
p.setLanguage(JavaScript2);
|
|
4673
4705
|
return p;
|
|
4674
4706
|
}
|
|
4707
|
+
function makePyParser2() {
|
|
4708
|
+
const p = new Parser2();
|
|
4709
|
+
p.setLanguage(Python2);
|
|
4710
|
+
return p;
|
|
4711
|
+
}
|
|
4675
4712
|
var ROUTER_METHODS = /* @__PURE__ */ new Set([
|
|
4676
4713
|
"get",
|
|
4677
4714
|
"post",
|
|
@@ -4684,6 +4721,7 @@ var ROUTER_METHODS = /* @__PURE__ */ new Set([
|
|
|
4684
4721
|
]);
|
|
4685
4722
|
var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
4686
4723
|
var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
|
|
4724
|
+
var FASTAPI_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
|
|
4687
4725
|
function canonicalizeTemplate(raw) {
|
|
4688
4726
|
let p = raw.split("?")[0].split("#")[0];
|
|
4689
4727
|
if (!p.startsWith("/")) p = "/" + p;
|
|
@@ -4911,8 +4949,102 @@ function nextRoutesFromFile(source, relFile, parser) {
|
|
|
4911
4949
|
}
|
|
4912
4950
|
return [];
|
|
4913
4951
|
}
|
|
4952
|
+
function pyStaticStringText(node) {
|
|
4953
|
+
if (node.type !== "string") return null;
|
|
4954
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
4955
|
+
const child = node.namedChild(i);
|
|
4956
|
+
if (child?.type === "interpolation") return null;
|
|
4957
|
+
if (child?.type === "string_content") return child.text;
|
|
4958
|
+
}
|
|
4959
|
+
return "";
|
|
4960
|
+
}
|
|
4961
|
+
function keywordArrayStrings(argsNode, key) {
|
|
4962
|
+
for (let i = 0; i < argsNode.namedChildCount; i++) {
|
|
4963
|
+
const arg = argsNode.namedChild(i);
|
|
4964
|
+
if (arg?.type !== "keyword_argument") continue;
|
|
4965
|
+
if (arg.childForFieldName("name")?.text !== key) continue;
|
|
4966
|
+
const val = arg.childForFieldName("value");
|
|
4967
|
+
if (!val || val.type !== "list") return [];
|
|
4968
|
+
const out = [];
|
|
4969
|
+
for (let j = 0; j < val.namedChildCount; j++) {
|
|
4970
|
+
const el = val.namedChild(j);
|
|
4971
|
+
if (el?.type === "string") {
|
|
4972
|
+
const s = pyStaticStringText(el);
|
|
4973
|
+
if (s) out.push(s);
|
|
4974
|
+
}
|
|
4975
|
+
}
|
|
4976
|
+
return out;
|
|
4977
|
+
}
|
|
4978
|
+
return [];
|
|
4979
|
+
}
|
|
4980
|
+
function collectApiRouterPrefixes(root) {
|
|
4981
|
+
const prefixes = /* @__PURE__ */ new Map();
|
|
4982
|
+
walk(root, (node) => {
|
|
4983
|
+
if (node.type !== "assignment") return;
|
|
4984
|
+
const right = node.childForFieldName("right");
|
|
4985
|
+
if (!right || right.type !== "call") return;
|
|
4986
|
+
const fn = right.childForFieldName("function");
|
|
4987
|
+
if (!fn) return;
|
|
4988
|
+
const ctor = fn.type === "attribute" ? fn.childForFieldName("attribute")?.text : fn.text;
|
|
4989
|
+
if (ctor !== "APIRouter") return;
|
|
4990
|
+
const left = node.childForFieldName("left");
|
|
4991
|
+
if (!left || left.type !== "identifier") return;
|
|
4992
|
+
const args = right.childForFieldName("arguments");
|
|
4993
|
+
if (!args) return;
|
|
4994
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
4995
|
+
const arg = args.namedChild(i);
|
|
4996
|
+
if (arg?.type !== "keyword_argument") continue;
|
|
4997
|
+
if (arg.childForFieldName("name")?.text !== "prefix") continue;
|
|
4998
|
+
const val = arg.childForFieldName("value");
|
|
4999
|
+
const p = val ? pyStaticStringText(val) : null;
|
|
5000
|
+
if (p !== null) prefixes.set(left.text, p);
|
|
5001
|
+
}
|
|
5002
|
+
});
|
|
5003
|
+
return prefixes;
|
|
5004
|
+
}
|
|
5005
|
+
function fastapiRoutesFromSource(source, parser) {
|
|
5006
|
+
const tree = parseSource2(parser, source);
|
|
5007
|
+
const prefixes = collectApiRouterPrefixes(tree.rootNode);
|
|
5008
|
+
const out = [];
|
|
5009
|
+
walk(tree.rootNode, (node) => {
|
|
5010
|
+
if (node.type !== "decorator") return;
|
|
5011
|
+
const call = node.namedChild(0);
|
|
5012
|
+
if (!call || call.type !== "call") return;
|
|
5013
|
+
const fn = call.childForFieldName("function");
|
|
5014
|
+
if (!fn || fn.type !== "attribute") return;
|
|
5015
|
+
const method = fn.childForFieldName("attribute")?.text?.toLowerCase();
|
|
5016
|
+
if (!method) return;
|
|
5017
|
+
const isVerb = FASTAPI_METHODS.has(method);
|
|
5018
|
+
if (!isVerb && method !== "api_route") return;
|
|
5019
|
+
const args = call.childForFieldName("arguments");
|
|
5020
|
+
const first = args?.namedChild(0);
|
|
5021
|
+
if (!first || first.type !== "string") return;
|
|
5022
|
+
const rawPath = pyStaticStringText(first);
|
|
5023
|
+
if (rawPath === null || !rawPath.startsWith("/")) return;
|
|
5024
|
+
const obj = fn.childForFieldName("object")?.text;
|
|
5025
|
+
const prefix = obj ? prefixes.get(obj) ?? "" : "";
|
|
5026
|
+
const pathTemplate = canonicalizeTemplate(prefix + rawPath);
|
|
5027
|
+
const line = node.startPosition.row + 1;
|
|
5028
|
+
if (isVerb) {
|
|
5029
|
+
out.push({ method: method.toUpperCase(), pathTemplate, line, framework: "fastapi" });
|
|
5030
|
+
return;
|
|
5031
|
+
}
|
|
5032
|
+
const methods = keywordArrayStrings(args, "methods");
|
|
5033
|
+
const list = methods.length > 0 ? methods : ["ALL"];
|
|
5034
|
+
for (const m of list) {
|
|
5035
|
+
out.push({
|
|
5036
|
+
method: m === "ALL" ? "ALL" : m.toUpperCase(),
|
|
5037
|
+
pathTemplate,
|
|
5038
|
+
line,
|
|
5039
|
+
framework: "fastapi"
|
|
5040
|
+
});
|
|
5041
|
+
}
|
|
5042
|
+
});
|
|
5043
|
+
return out;
|
|
5044
|
+
}
|
|
4914
5045
|
async function addRoutes(graph, services) {
|
|
4915
5046
|
const jsParser = makeJsParser2();
|
|
5047
|
+
const pyParser = makePyParser2();
|
|
4916
5048
|
let nodesAdded = 0;
|
|
4917
5049
|
let edgesAdded = 0;
|
|
4918
5050
|
for (const service of services) {
|
|
@@ -4924,15 +5056,20 @@ async function addRoutes(graph, services) {
|
|
|
4924
5056
|
const hasFastify = deps["fastify"] !== void 0;
|
|
4925
5057
|
const hasHono = deps["hono"] !== void 0;
|
|
4926
5058
|
const hasNext = deps["next"] !== void 0;
|
|
4927
|
-
|
|
5059
|
+
const hasFastapi = deps["fastapi"] !== void 0;
|
|
5060
|
+
if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasFastapi) continue;
|
|
4928
5061
|
const files = await loadSourceFiles(service.dir);
|
|
4929
5062
|
for (const file of files) {
|
|
4930
5063
|
if (isTestPath(file.path)) continue;
|
|
4931
|
-
|
|
5064
|
+
const ext = path22.extname(file.path);
|
|
5065
|
+
const isPy = ext === ".py";
|
|
5066
|
+
if (!JS_ROUTE_EXTENSIONS.has(ext) && !isPy) continue;
|
|
4932
5067
|
const relFile = toPosix2(path22.relative(service.dir, file.path));
|
|
4933
5068
|
let routes;
|
|
4934
5069
|
try {
|
|
4935
|
-
if (
|
|
5070
|
+
if (isPy) {
|
|
5071
|
+
routes = hasFastapi ? fastapiRoutesFromSource(file.content, pyParser) : [];
|
|
5072
|
+
} else if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
|
|
4936
5073
|
routes = nextRoutesFromFile(file.content, relFile, jsParser);
|
|
4937
5074
|
} else if (hasExpress || hasFastify || hasHono) {
|
|
4938
5075
|
routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify, hasHono);
|
|
@@ -5034,20 +5171,20 @@ function grpcMethodsFromProto(content, fqPackage) {
|
|
|
5034
5171
|
}
|
|
5035
5172
|
async function walkProtoFiles(dir) {
|
|
5036
5173
|
const out = [];
|
|
5037
|
-
async function
|
|
5174
|
+
async function walk4(current) {
|
|
5038
5175
|
const entries = await fs15.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
5039
5176
|
for (const entry of entries) {
|
|
5040
5177
|
const full = path23.join(current, entry.name);
|
|
5041
5178
|
if (entry.isDirectory()) {
|
|
5042
5179
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
5043
5180
|
if (await isPythonVenvDir(full)) continue;
|
|
5044
|
-
await
|
|
5181
|
+
await walk4(full);
|
|
5045
5182
|
} else if (entry.isFile() && path23.extname(entry.name) === PROTO_EXTENSION) {
|
|
5046
5183
|
out.push(full);
|
|
5047
5184
|
}
|
|
5048
5185
|
}
|
|
5049
5186
|
}
|
|
5050
|
-
await
|
|
5187
|
+
await walk4(dir);
|
|
5051
5188
|
return out;
|
|
5052
5189
|
}
|
|
5053
5190
|
async function addGrpcMethods(graph, services) {
|
|
@@ -5126,7 +5263,7 @@ import {
|
|
|
5126
5263
|
import path24 from "path";
|
|
5127
5264
|
import Parser3 from "tree-sitter";
|
|
5128
5265
|
import JavaScript3 from "tree-sitter-javascript";
|
|
5129
|
-
import
|
|
5266
|
+
import Python3 from "tree-sitter-python";
|
|
5130
5267
|
import {
|
|
5131
5268
|
EdgeType as EdgeType10,
|
|
5132
5269
|
Provenance as Provenance9,
|
|
@@ -5186,14 +5323,14 @@ function makeJsParser3() {
|
|
|
5186
5323
|
p.setLanguage(JavaScript3);
|
|
5187
5324
|
return p;
|
|
5188
5325
|
}
|
|
5189
|
-
function
|
|
5326
|
+
function makePyParser3() {
|
|
5190
5327
|
const p = new Parser3();
|
|
5191
|
-
p.setLanguage(
|
|
5328
|
+
p.setLanguage(Python3);
|
|
5192
5329
|
return p;
|
|
5193
5330
|
}
|
|
5194
5331
|
async function addHttpCallEdges(graph, services) {
|
|
5195
5332
|
const jsParser = makeJsParser3();
|
|
5196
|
-
const pyParser =
|
|
5333
|
+
const pyParser = makePyParser3();
|
|
5197
5334
|
const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
|
|
5198
5335
|
let nodesAdded = 0;
|
|
5199
5336
|
let edgesAdded = 0;
|
|
@@ -6133,6 +6270,120 @@ async function mongooseCrossFileEndpoints(files, serviceDir) {
|
|
|
6133
6270
|
return out;
|
|
6134
6271
|
}
|
|
6135
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
|
+
|
|
6136
6387
|
// src/extract/calls/index.ts
|
|
6137
6388
|
function edgeTypeFromEndpoint(ep) {
|
|
6138
6389
|
switch (ep.edgeType) {
|
|
@@ -6165,6 +6416,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
6165
6416
|
endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
|
|
6166
6417
|
endpoints.push(...supabaseEndpointsFromFile(maskedFile, service.dir));
|
|
6167
6418
|
endpoints.push(...mongooseEndpointsFromFile(maskedFile, service.dir));
|
|
6419
|
+
endpoints.push(...sqlalchemyEndpointsFromFile(maskedFile, service.dir));
|
|
6168
6420
|
}
|
|
6169
6421
|
endpoints.push(...await mongooseCrossFileEndpoints(maskedFiles, service.dir));
|
|
6170
6422
|
if (endpoints.length === 0) continue;
|
|
@@ -6237,14 +6489,14 @@ async function addCallEdges(graph, services) {
|
|
|
6237
6489
|
}
|
|
6238
6490
|
|
|
6239
6491
|
// src/extract/infra/docker-compose.ts
|
|
6240
|
-
import
|
|
6492
|
+
import path33 from "path";
|
|
6241
6493
|
import { EdgeType as EdgeType13, Provenance as Provenance13, confidenceForExtracted as confidenceForExtracted11 } from "@neat.is/types";
|
|
6242
6494
|
|
|
6243
6495
|
// src/extract/infra/shared.ts
|
|
6244
|
-
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";
|
|
6245
6497
|
function makeInfraNode(kind, name, provider = "self", extras) {
|
|
6246
6498
|
return {
|
|
6247
|
-
id:
|
|
6499
|
+
id: infraId9(kind, name),
|
|
6248
6500
|
type: NodeType13.InfraNode,
|
|
6249
6501
|
name,
|
|
6250
6502
|
provider,
|
|
@@ -6307,7 +6559,7 @@ function dependsOnList(value) {
|
|
|
6307
6559
|
}
|
|
6308
6560
|
function serviceNameToServiceNode(name, services) {
|
|
6309
6561
|
for (const s of services) {
|
|
6310
|
-
if (s.node.name === name ||
|
|
6562
|
+
if (s.node.name === name || path33.basename(s.dir) === name) return s.node.id;
|
|
6311
6563
|
}
|
|
6312
6564
|
return null;
|
|
6313
6565
|
}
|
|
@@ -6316,7 +6568,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
6316
6568
|
let edgesAdded = 0;
|
|
6317
6569
|
let composePath = null;
|
|
6318
6570
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
6319
|
-
const abs =
|
|
6571
|
+
const abs = path33.join(scanPath, name);
|
|
6320
6572
|
if (await exists(abs)) {
|
|
6321
6573
|
composePath = abs;
|
|
6322
6574
|
break;
|
|
@@ -6329,13 +6581,13 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
6329
6581
|
} catch (err) {
|
|
6330
6582
|
recordExtractionError(
|
|
6331
6583
|
"infra docker-compose",
|
|
6332
|
-
|
|
6584
|
+
path33.relative(scanPath, composePath),
|
|
6333
6585
|
err
|
|
6334
6586
|
);
|
|
6335
6587
|
return { nodesAdded, edgesAdded };
|
|
6336
6588
|
}
|
|
6337
6589
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
6338
|
-
const evidenceFile =
|
|
6590
|
+
const evidenceFile = path33.relative(scanPath, composePath).split(path33.sep).join("/");
|
|
6339
6591
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
6340
6592
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
6341
6593
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -6376,7 +6628,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
6376
6628
|
}
|
|
6377
6629
|
|
|
6378
6630
|
// src/extract/infra/dockerfile.ts
|
|
6379
|
-
import
|
|
6631
|
+
import path34 from "path";
|
|
6380
6632
|
import { promises as fs16 } from "fs";
|
|
6381
6633
|
import { EdgeType as EdgeType14, Provenance as Provenance14, confidenceForExtracted as confidenceForExtracted12 } from "@neat.is/types";
|
|
6382
6634
|
function readDockerfile(content) {
|
|
@@ -6407,7 +6659,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
6407
6659
|
let nodesAdded = 0;
|
|
6408
6660
|
let edgesAdded = 0;
|
|
6409
6661
|
for (const service of services) {
|
|
6410
|
-
const dockerfilePath =
|
|
6662
|
+
const dockerfilePath = path34.join(service.dir, "Dockerfile");
|
|
6411
6663
|
if (!await exists(dockerfilePath)) continue;
|
|
6412
6664
|
let content;
|
|
6413
6665
|
try {
|
|
@@ -6415,7 +6667,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
6415
6667
|
} catch (err) {
|
|
6416
6668
|
recordExtractionError(
|
|
6417
6669
|
"infra dockerfile",
|
|
6418
|
-
|
|
6670
|
+
path34.relative(scanPath, dockerfilePath),
|
|
6419
6671
|
err
|
|
6420
6672
|
);
|
|
6421
6673
|
continue;
|
|
@@ -6427,8 +6679,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
6427
6679
|
graph.addNode(node.id, node);
|
|
6428
6680
|
nodesAdded++;
|
|
6429
6681
|
}
|
|
6430
|
-
const relDockerfile = toPosix2(
|
|
6431
|
-
const evidenceFile = toPosix2(
|
|
6682
|
+
const relDockerfile = toPosix2(path34.relative(service.dir, dockerfilePath));
|
|
6683
|
+
const evidenceFile = toPosix2(path34.relative(scanPath, dockerfilePath));
|
|
6432
6684
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
6433
6685
|
graph,
|
|
6434
6686
|
service.pkg.name,
|
|
@@ -6480,7 +6732,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
6480
6732
|
|
|
6481
6733
|
// src/extract/infra/terraform.ts
|
|
6482
6734
|
import { promises as fs17 } from "fs";
|
|
6483
|
-
import
|
|
6735
|
+
import path35 from "path";
|
|
6484
6736
|
import { EdgeType as EdgeType15, Provenance as Provenance15, confidenceForExtracted as confidenceForExtracted13 } from "@neat.is/types";
|
|
6485
6737
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
6486
6738
|
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
@@ -6491,11 +6743,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
6491
6743
|
for (const entry of entries) {
|
|
6492
6744
|
if (entry.isDirectory()) {
|
|
6493
6745
|
if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
|
|
6494
|
-
const child =
|
|
6746
|
+
const child = path35.join(start, entry.name);
|
|
6495
6747
|
if (await isPythonVenvDir(child)) continue;
|
|
6496
6748
|
out.push(...await walkTfFiles(child, depth + 1, max));
|
|
6497
6749
|
} else if (entry.isFile() && entry.name.endsWith(".tf")) {
|
|
6498
|
-
out.push(
|
|
6750
|
+
out.push(path35.join(start, entry.name));
|
|
6499
6751
|
}
|
|
6500
6752
|
}
|
|
6501
6753
|
return out;
|
|
@@ -6527,7 +6779,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
6527
6779
|
const files = await walkTfFiles(scanPath);
|
|
6528
6780
|
for (const file of files) {
|
|
6529
6781
|
const content = await fs17.readFile(file, "utf8");
|
|
6530
|
-
const evidenceFile = toPosix2(
|
|
6782
|
+
const evidenceFile = toPosix2(path35.relative(scanPath, file));
|
|
6531
6783
|
const resources = [];
|
|
6532
6784
|
const byKey = /* @__PURE__ */ new Map();
|
|
6533
6785
|
RESOURCE_RE.lastIndex = 0;
|
|
@@ -6584,7 +6836,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
6584
6836
|
|
|
6585
6837
|
// src/extract/infra/k8s.ts
|
|
6586
6838
|
import { promises as fs18 } from "fs";
|
|
6587
|
-
import
|
|
6839
|
+
import path36 from "path";
|
|
6588
6840
|
import { parseAllDocuments as parseAllDocuments2 } from "yaml";
|
|
6589
6841
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
6590
6842
|
Service: "k8s-service",
|
|
@@ -6602,11 +6854,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
|
6602
6854
|
for (const entry of entries) {
|
|
6603
6855
|
if (entry.isDirectory()) {
|
|
6604
6856
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
6605
|
-
const child =
|
|
6857
|
+
const child = path36.join(start, entry.name);
|
|
6606
6858
|
if (await isPythonVenvDir(child)) continue;
|
|
6607
6859
|
out.push(...await walkYamlFiles2(child, depth + 1, max));
|
|
6608
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
6609
|
-
out.push(
|
|
6860
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path36.extname(entry.name))) {
|
|
6861
|
+
out.push(path36.join(start, entry.name));
|
|
6610
6862
|
}
|
|
6611
6863
|
}
|
|
6612
6864
|
return out;
|
|
@@ -6639,13 +6891,13 @@ async function addK8sResources(graph, scanPath) {
|
|
|
6639
6891
|
|
|
6640
6892
|
// src/extract/infra/cloudflare.ts
|
|
6641
6893
|
import { promises as fs19 } from "fs";
|
|
6642
|
-
import
|
|
6894
|
+
import path37 from "path";
|
|
6643
6895
|
import { parse as parseToml2 } from "smol-toml";
|
|
6644
6896
|
import { EdgeType as EdgeType16, Provenance as Provenance16, confidenceForExtracted as confidenceForExtracted14 } from "@neat.is/types";
|
|
6645
6897
|
var WRANGLER_FILENAMES = ["wrangler.toml", "wrangler.jsonc", "wrangler.json"];
|
|
6646
6898
|
async function readWranglerConfig(dir) {
|
|
6647
6899
|
for (const filename of WRANGLER_FILENAMES) {
|
|
6648
|
-
const abs =
|
|
6900
|
+
const abs = path37.join(dir, filename);
|
|
6649
6901
|
if (!await exists(abs)) continue;
|
|
6650
6902
|
const raw = await fs19.readFile(abs, "utf8");
|
|
6651
6903
|
const config = filename === "wrangler.toml" ? parseToml2(raw) : JSON.parse(maskCommentsInSource(raw));
|
|
@@ -6708,11 +6960,11 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
6708
6960
|
try {
|
|
6709
6961
|
read = await readWranglerConfig(service.dir);
|
|
6710
6962
|
} catch (err) {
|
|
6711
|
-
recordExtractionError("infra cloudflare",
|
|
6963
|
+
recordExtractionError("infra cloudflare", path37.relative(scanPath, service.dir), err);
|
|
6712
6964
|
continue;
|
|
6713
6965
|
}
|
|
6714
6966
|
if (!read || !read.config.name) continue;
|
|
6715
|
-
const evidenceFile = toPosix2(
|
|
6967
|
+
const evidenceFile = toPosix2(path37.relative(scanPath, path37.join(service.dir, read.relFile)));
|
|
6716
6968
|
discovered.push({ service, config: read.config, relFile: read.relFile, raw: read.raw, evidenceFile });
|
|
6717
6969
|
}
|
|
6718
6970
|
for (const worker of discovered) {
|
|
@@ -6724,7 +6976,7 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
6724
6976
|
}
|
|
6725
6977
|
let anchorId = service.node.id;
|
|
6726
6978
|
if (config.main) {
|
|
6727
|
-
const entryRelPath = toPosix2(
|
|
6979
|
+
const entryRelPath = toPosix2(path37.normalize(config.main));
|
|
6728
6980
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
6729
6981
|
graph,
|
|
6730
6982
|
service.pkg.name,
|
|
@@ -6871,12 +7123,12 @@ async function addCloudflareWorkers(graph, services, scanPath) {
|
|
|
6871
7123
|
|
|
6872
7124
|
// src/extract/infra/vercel.ts
|
|
6873
7125
|
import { promises as fs20 } from "fs";
|
|
6874
|
-
import
|
|
7126
|
+
import path38 from "path";
|
|
6875
7127
|
import { EdgeType as EdgeType17 } from "@neat.is/types";
|
|
6876
7128
|
var VERCEL_CONFIG_FILENAMES = ["vercel.json", "vercel.jsonc"];
|
|
6877
7129
|
async function readVercelConfig(dir) {
|
|
6878
7130
|
for (const filename of VERCEL_CONFIG_FILENAMES) {
|
|
6879
|
-
const abs =
|
|
7131
|
+
const abs = path38.join(dir, filename);
|
|
6880
7132
|
if (!await exists(abs)) continue;
|
|
6881
7133
|
const raw = await fs20.readFile(abs, "utf8");
|
|
6882
7134
|
const config = JSON.parse(maskCommentsInSource(raw));
|
|
@@ -6885,7 +7137,7 @@ async function readVercelConfig(dir) {
|
|
|
6885
7137
|
return null;
|
|
6886
7138
|
}
|
|
6887
7139
|
async function readLinkedProjectName(dir) {
|
|
6888
|
-
const abs =
|
|
7140
|
+
const abs = path38.join(dir, ".vercel", "project.json");
|
|
6889
7141
|
if (!await exists(abs)) return void 0;
|
|
6890
7142
|
const parsed = JSON.parse(await fs20.readFile(abs, "utf8"));
|
|
6891
7143
|
return typeof parsed.projectName === "string" ? parsed.projectName : void 0;
|
|
@@ -6903,7 +7155,7 @@ async function addVercelServices(graph, services, scanPath) {
|
|
|
6903
7155
|
read = await readVercelConfig(service.dir);
|
|
6904
7156
|
projectName = await readLinkedProjectName(service.dir);
|
|
6905
7157
|
} catch (err) {
|
|
6906
|
-
recordExtractionError("infra vercel",
|
|
7158
|
+
recordExtractionError("infra vercel", path38.relative(scanPath, service.dir), err);
|
|
6907
7159
|
continue;
|
|
6908
7160
|
}
|
|
6909
7161
|
if (!read && !projectName) continue;
|
|
@@ -6919,7 +7171,7 @@ async function addVercelServices(graph, services, scanPath) {
|
|
|
6919
7171
|
const anchorId = service.node.id;
|
|
6920
7172
|
if (!read) continue;
|
|
6921
7173
|
const { config, relFile, raw } = read;
|
|
6922
|
-
const evidenceFile = toPosix2(
|
|
7174
|
+
const evidenceFile = toPosix2(path38.relative(scanPath, path38.join(service.dir, relFile)));
|
|
6923
7175
|
const add = (edgeType, kind, name) => {
|
|
6924
7176
|
if (!name) return;
|
|
6925
7177
|
const result = emitPlatformResourceEdge(
|
|
@@ -6948,13 +7200,13 @@ async function addVercelServices(graph, services, scanPath) {
|
|
|
6948
7200
|
|
|
6949
7201
|
// src/extract/infra/railway.ts
|
|
6950
7202
|
import { promises as fs21 } from "fs";
|
|
6951
|
-
import
|
|
7203
|
+
import path39 from "path";
|
|
6952
7204
|
import { parse as parseToml3 } from "smol-toml";
|
|
6953
7205
|
import { EdgeType as EdgeType18 } from "@neat.is/types";
|
|
6954
7206
|
var RAILWAY_FILENAMES = ["railway.toml", "railway.json", "railway.jsonc"];
|
|
6955
7207
|
async function readRailwayConfig(dir) {
|
|
6956
7208
|
for (const filename of RAILWAY_FILENAMES) {
|
|
6957
|
-
const abs =
|
|
7209
|
+
const abs = path39.join(dir, filename);
|
|
6958
7210
|
if (!await exists(abs)) continue;
|
|
6959
7211
|
const raw = await fs21.readFile(abs, "utf8");
|
|
6960
7212
|
const config = filename === "railway.toml" ? parseToml3(raw) : JSON.parse(maskCommentsInSource(raw));
|
|
@@ -6970,7 +7222,7 @@ async function addRailwayServices(graph, services, scanPath) {
|
|
|
6970
7222
|
try {
|
|
6971
7223
|
read = await readRailwayConfig(service.dir);
|
|
6972
7224
|
} catch (err) {
|
|
6973
|
-
recordExtractionError("infra railway",
|
|
7225
|
+
recordExtractionError("infra railway", path39.relative(scanPath, service.dir), err);
|
|
6974
7226
|
continue;
|
|
6975
7227
|
}
|
|
6976
7228
|
if (!read) continue;
|
|
@@ -6980,7 +7232,7 @@ async function addRailwayServices(graph, services, scanPath) {
|
|
|
6980
7232
|
}
|
|
6981
7233
|
const anchorId = service.node.id;
|
|
6982
7234
|
const { config, relFile, raw } = read;
|
|
6983
|
-
const evidenceFile = toPosix2(
|
|
7235
|
+
const evidenceFile = toPosix2(path39.relative(scanPath, path39.join(service.dir, relFile)));
|
|
6984
7236
|
const add = (edgeType, kind, name) => {
|
|
6985
7237
|
if (!name) return;
|
|
6986
7238
|
const result = emitPlatformResourceEdge(
|
|
@@ -7005,12 +7257,12 @@ async function addRailwayServices(graph, services, scanPath) {
|
|
|
7005
7257
|
|
|
7006
7258
|
// src/extract/infra/supabase.ts
|
|
7007
7259
|
import { promises as fs22 } from "fs";
|
|
7008
|
-
import
|
|
7260
|
+
import path40 from "path";
|
|
7009
7261
|
import { parse as parseToml4 } from "smol-toml";
|
|
7010
7262
|
import { EdgeType as EdgeType19 } from "@neat.is/types";
|
|
7011
7263
|
async function readSupabaseConfig(dir) {
|
|
7012
|
-
const relFile =
|
|
7013
|
-
const abs =
|
|
7264
|
+
const relFile = path40.join("supabase", "config.toml");
|
|
7265
|
+
const abs = path40.join(dir, relFile);
|
|
7014
7266
|
if (!await exists(abs)) return null;
|
|
7015
7267
|
const raw = await fs22.readFile(abs, "utf8");
|
|
7016
7268
|
const config = parseToml4(raw);
|
|
@@ -7024,7 +7276,7 @@ async function addSupabaseProjects(graph, services, scanPath) {
|
|
|
7024
7276
|
try {
|
|
7025
7277
|
read = await readSupabaseConfig(service.dir);
|
|
7026
7278
|
} catch (err) {
|
|
7027
|
-
recordExtractionError("infra supabase",
|
|
7279
|
+
recordExtractionError("infra supabase", path40.relative(scanPath, service.dir), err);
|
|
7028
7280
|
continue;
|
|
7029
7281
|
}
|
|
7030
7282
|
if (!read) continue;
|
|
@@ -7039,7 +7291,7 @@ async function addSupabaseProjects(graph, services, scanPath) {
|
|
|
7039
7291
|
});
|
|
7040
7292
|
}
|
|
7041
7293
|
const anchorId = service.node.id;
|
|
7042
|
-
const evidenceFile = toPosix2(
|
|
7294
|
+
const evidenceFile = toPosix2(path40.relative(scanPath, path40.join(service.dir, relFile)));
|
|
7043
7295
|
const add = (edgeType, kind, name) => {
|
|
7044
7296
|
if (!name) return;
|
|
7045
7297
|
const result = emitPlatformResourceEdge(
|
|
@@ -7080,11 +7332,11 @@ async function addInfra(graph, scanPath, services) {
|
|
|
7080
7332
|
}
|
|
7081
7333
|
|
|
7082
7334
|
// src/extract/index.ts
|
|
7083
|
-
import
|
|
7335
|
+
import path42 from "path";
|
|
7084
7336
|
|
|
7085
7337
|
// src/extract/retire.ts
|
|
7086
7338
|
import { existsSync as existsSync2 } from "fs";
|
|
7087
|
-
import
|
|
7339
|
+
import path41 from "path";
|
|
7088
7340
|
import { NodeType as NodeType14, Provenance as Provenance17 } from "@neat.is/types";
|
|
7089
7341
|
function dropOrphanedFileNodes(graph) {
|
|
7090
7342
|
const orphans = [];
|
|
@@ -7118,11 +7370,11 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
7118
7370
|
if (edge.provenance !== Provenance17.EXTRACTED) return;
|
|
7119
7371
|
const evidenceFile = edge.evidence?.file;
|
|
7120
7372
|
if (!evidenceFile) return;
|
|
7121
|
-
if (
|
|
7373
|
+
if (path41.isAbsolute(evidenceFile)) {
|
|
7122
7374
|
if (!existsSync2(evidenceFile)) toDrop.push(id);
|
|
7123
7375
|
return;
|
|
7124
7376
|
}
|
|
7125
|
-
const found = bases.some((base) => existsSync2(
|
|
7377
|
+
const found = bases.some((base) => existsSync2(path41.join(base, evidenceFile)));
|
|
7126
7378
|
if (!found) toDrop.push(id);
|
|
7127
7379
|
});
|
|
7128
7380
|
for (const id of toDrop) graph.dropEdge(id);
|
|
@@ -7164,7 +7416,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
7164
7416
|
}
|
|
7165
7417
|
const droppedEntries = drainDroppedExtracted();
|
|
7166
7418
|
if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
|
|
7167
|
-
const rejectedPath =
|
|
7419
|
+
const rejectedPath = path42.join(path42.dirname(opts.errorsPath), "rejected.ndjson");
|
|
7168
7420
|
try {
|
|
7169
7421
|
await writeRejectedExtracted(droppedEntries, rejectedPath);
|
|
7170
7422
|
} catch (err) {
|
|
@@ -7482,7 +7734,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
7482
7734
|
|
|
7483
7735
|
// src/persist.ts
|
|
7484
7736
|
import { promises as fs23 } from "fs";
|
|
7485
|
-
import
|
|
7737
|
+
import path43 from "path";
|
|
7486
7738
|
import { Provenance as Provenance19, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
|
|
7487
7739
|
var SCHEMA_VERSION = 4;
|
|
7488
7740
|
function migrateV1ToV2(payload) {
|
|
@@ -7519,7 +7771,7 @@ function migrateV2ToV3(payload) {
|
|
|
7519
7771
|
return { ...payload, schemaVersion: 3 };
|
|
7520
7772
|
}
|
|
7521
7773
|
async function ensureDir(filePath) {
|
|
7522
|
-
await fs23.mkdir(
|
|
7774
|
+
await fs23.mkdir(path43.dirname(filePath), { recursive: true });
|
|
7523
7775
|
}
|
|
7524
7776
|
async function saveGraphToDisk(graph, outPath) {
|
|
7525
7777
|
await ensureDir(outPath);
|
|
@@ -7675,23 +7927,23 @@ function canonicalJson(value) {
|
|
|
7675
7927
|
}
|
|
7676
7928
|
|
|
7677
7929
|
// src/projects.ts
|
|
7678
|
-
import
|
|
7930
|
+
import path44 from "path";
|
|
7679
7931
|
function pathsForProject(project, baseDir) {
|
|
7680
7932
|
if (project === DEFAULT_PROJECT) {
|
|
7681
7933
|
return {
|
|
7682
|
-
snapshotPath:
|
|
7683
|
-
errorsPath:
|
|
7684
|
-
staleEventsPath:
|
|
7685
|
-
embeddingsCachePath:
|
|
7686
|
-
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")
|
|
7687
7939
|
};
|
|
7688
7940
|
}
|
|
7689
7941
|
return {
|
|
7690
|
-
snapshotPath:
|
|
7691
|
-
errorsPath:
|
|
7692
|
-
staleEventsPath:
|
|
7693
|
-
embeddingsCachePath:
|
|
7694
|
-
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`)
|
|
7695
7947
|
};
|
|
7696
7948
|
}
|
|
7697
7949
|
var Projects = class {
|
|
@@ -7732,7 +7984,7 @@ function parseExtraProjects(raw) {
|
|
|
7732
7984
|
// src/registry.ts
|
|
7733
7985
|
import { promises as fs25 } from "fs";
|
|
7734
7986
|
import os2 from "os";
|
|
7735
|
-
import
|
|
7987
|
+
import path45 from "path";
|
|
7736
7988
|
import {
|
|
7737
7989
|
RegistryFileSchema
|
|
7738
7990
|
} from "@neat.is/types";
|
|
@@ -7740,20 +7992,20 @@ var LOCK_TIMEOUT_MS = 5e3;
|
|
|
7740
7992
|
var LOCK_RETRY_MS = 50;
|
|
7741
7993
|
function neatHome() {
|
|
7742
7994
|
const override = process.env.NEAT_HOME;
|
|
7743
|
-
if (override && override.length > 0) return
|
|
7744
|
-
return
|
|
7995
|
+
if (override && override.length > 0) return path45.resolve(override);
|
|
7996
|
+
return path45.join(os2.homedir(), ".neat");
|
|
7745
7997
|
}
|
|
7746
7998
|
function registryPath() {
|
|
7747
|
-
return
|
|
7999
|
+
return path45.join(neatHome(), "projects.json");
|
|
7748
8000
|
}
|
|
7749
8001
|
function registryLockPath() {
|
|
7750
|
-
return
|
|
8002
|
+
return path45.join(neatHome(), "projects.json.lock");
|
|
7751
8003
|
}
|
|
7752
8004
|
function daemonPidPath() {
|
|
7753
|
-
return
|
|
8005
|
+
return path45.join(neatHome(), "neatd.pid");
|
|
7754
8006
|
}
|
|
7755
8007
|
function daemonsDir() {
|
|
7756
|
-
return
|
|
8008
|
+
return path45.join(neatHome(), "daemons");
|
|
7757
8009
|
}
|
|
7758
8010
|
function isFiniteInt(v) {
|
|
7759
8011
|
return typeof v === "number" && Number.isFinite(v);
|
|
@@ -7794,7 +8046,7 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
|
|
|
7794
8046
|
const out = [];
|
|
7795
8047
|
for (const name of names) {
|
|
7796
8048
|
if (!name.endsWith(".json")) continue;
|
|
7797
|
-
const file =
|
|
8049
|
+
const file = path45.join(dir, name);
|
|
7798
8050
|
let raw;
|
|
7799
8051
|
try {
|
|
7800
8052
|
raw = await fs25.readFile(file, "utf8");
|
|
@@ -7915,7 +8167,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
|
|
|
7915
8167
|
}
|
|
7916
8168
|
}
|
|
7917
8169
|
async function normalizeProjectPath(input) {
|
|
7918
|
-
const resolved =
|
|
8170
|
+
const resolved = path45.resolve(input);
|
|
7919
8171
|
try {
|
|
7920
8172
|
return await fs25.realpath(resolved);
|
|
7921
8173
|
} catch {
|
|
@@ -7923,7 +8175,7 @@ async function normalizeProjectPath(input) {
|
|
|
7923
8175
|
}
|
|
7924
8176
|
}
|
|
7925
8177
|
async function writeAtomically(target, contents) {
|
|
7926
|
-
await fs25.mkdir(
|
|
8178
|
+
await fs25.mkdir(path45.dirname(target), { recursive: true });
|
|
7927
8179
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
7928
8180
|
const fd = await fs25.open(tmp, "w");
|
|
7929
8181
|
try {
|
|
@@ -7936,7 +8188,7 @@ async function writeAtomically(target, contents) {
|
|
|
7936
8188
|
}
|
|
7937
8189
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
|
|
7938
8190
|
const deadline = Date.now() + timeoutMs;
|
|
7939
|
-
await fs25.mkdir(
|
|
8191
|
+
await fs25.mkdir(path45.dirname(lockPath), { recursive: true });
|
|
7940
8192
|
let probedHolder = false;
|
|
7941
8193
|
while (true) {
|
|
7942
8194
|
try {
|
|
@@ -8132,13 +8384,13 @@ import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } f
|
|
|
8132
8384
|
|
|
8133
8385
|
// src/extend/index.ts
|
|
8134
8386
|
import { promises as fs27 } from "fs";
|
|
8135
|
-
import
|
|
8387
|
+
import path47 from "path";
|
|
8136
8388
|
import os3 from "os";
|
|
8137
8389
|
import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
|
|
8138
8390
|
|
|
8139
8391
|
// src/installers/package-manager.ts
|
|
8140
8392
|
import { promises as fs26 } from "fs";
|
|
8141
|
-
import
|
|
8393
|
+
import path46 from "path";
|
|
8142
8394
|
import { spawn } from "child_process";
|
|
8143
8395
|
var LOCKFILE_PRIORITY = [
|
|
8144
8396
|
{ lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
|
|
@@ -8160,22 +8412,22 @@ async function exists2(p) {
|
|
|
8160
8412
|
}
|
|
8161
8413
|
}
|
|
8162
8414
|
async function detectPackageManager(serviceDir) {
|
|
8163
|
-
let dir =
|
|
8415
|
+
let dir = path46.resolve(serviceDir);
|
|
8164
8416
|
const stops = /* @__PURE__ */ new Set();
|
|
8165
8417
|
for (let i = 0; i < 64; i++) {
|
|
8166
8418
|
if (stops.has(dir)) break;
|
|
8167
8419
|
stops.add(dir);
|
|
8168
8420
|
for (const candidate of LOCKFILE_PRIORITY) {
|
|
8169
|
-
const lockPath =
|
|
8421
|
+
const lockPath = path46.join(dir, candidate.lockfile);
|
|
8170
8422
|
if (await exists2(lockPath)) {
|
|
8171
8423
|
return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
|
|
8172
8424
|
}
|
|
8173
8425
|
}
|
|
8174
|
-
const parent =
|
|
8426
|
+
const parent = path46.dirname(dir);
|
|
8175
8427
|
if (parent === dir) break;
|
|
8176
8428
|
dir = parent;
|
|
8177
8429
|
}
|
|
8178
|
-
return { pm: "npm", cwd:
|
|
8430
|
+
return { pm: "npm", cwd: path46.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
|
|
8179
8431
|
}
|
|
8180
8432
|
async function runPackageManagerInstall(cmd) {
|
|
8181
8433
|
return new Promise((resolve) => {
|
|
@@ -8224,7 +8476,7 @@ async function fileExists2(p) {
|
|
|
8224
8476
|
}
|
|
8225
8477
|
}
|
|
8226
8478
|
async function readPackageJson(scanPath) {
|
|
8227
|
-
const pkgPath =
|
|
8479
|
+
const pkgPath = path47.join(scanPath, "package.json");
|
|
8228
8480
|
const raw = await fs27.readFile(pkgPath, "utf8");
|
|
8229
8481
|
return JSON.parse(raw);
|
|
8230
8482
|
}
|
|
@@ -8238,27 +8490,27 @@ var HOOK_WALK_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
|
8238
8490
|
]);
|
|
8239
8491
|
async function findHookFiles(scanPath) {
|
|
8240
8492
|
const found = [];
|
|
8241
|
-
const
|
|
8493
|
+
const walk4 = async (dir) => {
|
|
8242
8494
|
const entries = await fs27.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
8243
8495
|
for (const entry of entries) {
|
|
8244
8496
|
if (entry.isDirectory()) {
|
|
8245
8497
|
if (entry.name.startsWith(".") || HOOK_WALK_SKIP_DIRS.has(entry.name)) continue;
|
|
8246
|
-
await
|
|
8498
|
+
await walk4(path47.join(dir, entry.name));
|
|
8247
8499
|
} else if (entry.isFile()) {
|
|
8248
8500
|
if ((entry.name.startsWith("instrumentation") || entry.name.startsWith("otel-init")) && /\.(ts|js|cjs|mjs)$/.test(entry.name)) {
|
|
8249
|
-
const rel =
|
|
8250
|
-
found.push(rel.split(
|
|
8501
|
+
const rel = path47.relative(scanPath, path47.join(dir, entry.name));
|
|
8502
|
+
found.push(rel.split(path47.sep).join("/"));
|
|
8251
8503
|
}
|
|
8252
8504
|
}
|
|
8253
8505
|
}
|
|
8254
8506
|
};
|
|
8255
|
-
await
|
|
8507
|
+
await walk4(scanPath);
|
|
8256
8508
|
return found.sort();
|
|
8257
8509
|
}
|
|
8258
8510
|
async function pickPrimaryHookFile(scanPath, hookFiles, snippet2) {
|
|
8259
8511
|
let fallback = null;
|
|
8260
8512
|
for (const file of hookFiles) {
|
|
8261
|
-
const content = await fs27.readFile(
|
|
8513
|
+
const content = await fs27.readFile(path47.join(scanPath, file), "utf8");
|
|
8262
8514
|
const patched = splicedContent(content, snippet2);
|
|
8263
8515
|
if (patched !== null) return { file, content, patched };
|
|
8264
8516
|
if (fallback === null) fallback = { file, content };
|
|
@@ -8266,11 +8518,11 @@ async function pickPrimaryHookFile(scanPath, hookFiles, snippet2) {
|
|
|
8266
8518
|
return { file: fallback.file, content: fallback.content, patched: null };
|
|
8267
8519
|
}
|
|
8268
8520
|
function extendLogPath() {
|
|
8269
|
-
return process.env.NEAT_EXTEND_LOG ??
|
|
8521
|
+
return process.env.NEAT_EXTEND_LOG ?? path47.join(os3.homedir(), ".neat", "extend-log.ndjson");
|
|
8270
8522
|
}
|
|
8271
8523
|
async function appendExtendLog(entry) {
|
|
8272
8524
|
const logPath = extendLogPath();
|
|
8273
|
-
await fs27.mkdir(
|
|
8525
|
+
await fs27.mkdir(path47.dirname(logPath), { recursive: true });
|
|
8274
8526
|
await fs27.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
|
|
8275
8527
|
}
|
|
8276
8528
|
function splicedContent(fileContent, snippet2) {
|
|
@@ -8329,7 +8581,7 @@ function lookupInstrumentation(library, installedVersion) {
|
|
|
8329
8581
|
}
|
|
8330
8582
|
async function describeProjectInstrumentation(ctx) {
|
|
8331
8583
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
8332
|
-
const envNeat = await fileExists2(
|
|
8584
|
+
const envNeat = await fileExists2(path47.join(ctx.scanPath, ".env.neat"));
|
|
8333
8585
|
const registryInstrPackages = new Set(
|
|
8334
8586
|
registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
|
|
8335
8587
|
);
|
|
@@ -8351,7 +8603,7 @@ async function applyExtension(ctx, args, options) {
|
|
|
8351
8603
|
);
|
|
8352
8604
|
}
|
|
8353
8605
|
for (const file of hookFiles) {
|
|
8354
|
-
const content = await fs27.readFile(
|
|
8606
|
+
const content = await fs27.readFile(path47.join(ctx.scanPath, file), "utf8");
|
|
8355
8607
|
if (content.includes(args.registration_snippet)) {
|
|
8356
8608
|
return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
|
|
8357
8609
|
}
|
|
@@ -8363,10 +8615,10 @@ async function applyExtension(ctx, args, options) {
|
|
|
8363
8615
|
);
|
|
8364
8616
|
}
|
|
8365
8617
|
const primaryFile = primary.file;
|
|
8366
|
-
const primaryPath =
|
|
8618
|
+
const primaryPath = path47.join(ctx.scanPath, primaryFile);
|
|
8367
8619
|
const filesTouched = [];
|
|
8368
8620
|
const depsAdded = [];
|
|
8369
|
-
const pkgPath =
|
|
8621
|
+
const pkgPath = path47.join(ctx.scanPath, "package.json");
|
|
8370
8622
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
8371
8623
|
if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
|
|
8372
8624
|
pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
|
|
@@ -8405,7 +8657,7 @@ async function dryRunExtension(ctx, args) {
|
|
|
8405
8657
|
};
|
|
8406
8658
|
}
|
|
8407
8659
|
for (const file of hookFiles) {
|
|
8408
|
-
const content = await fs27.readFile(
|
|
8660
|
+
const content = await fs27.readFile(path47.join(ctx.scanPath, file), "utf8");
|
|
8409
8661
|
if (content.includes(args.registration_snippet)) {
|
|
8410
8662
|
return {
|
|
8411
8663
|
library: args.library,
|
|
@@ -8446,7 +8698,7 @@ async function rollbackExtension(ctx, args) {
|
|
|
8446
8698
|
if (!match) {
|
|
8447
8699
|
return { undone: false, message: "no apply found for library" };
|
|
8448
8700
|
}
|
|
8449
|
-
const pkgPath =
|
|
8701
|
+
const pkgPath = path47.join(ctx.scanPath, "package.json");
|
|
8450
8702
|
if (await fileExists2(pkgPath)) {
|
|
8451
8703
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
8452
8704
|
if (pkg.dependencies?.[match.instrumentation_package]) {
|
|
@@ -8457,7 +8709,7 @@ async function rollbackExtension(ctx, args) {
|
|
|
8457
8709
|
}
|
|
8458
8710
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
8459
8711
|
for (const file of hookFiles) {
|
|
8460
|
-
const filePath =
|
|
8712
|
+
const filePath = path47.join(ctx.scanPath, file);
|
|
8461
8713
|
const content = await fs27.readFile(filePath, "utf8");
|
|
8462
8714
|
if (content.includes(match.registration_snippet)) {
|
|
8463
8715
|
const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
|
|
@@ -8572,7 +8824,7 @@ data: ${JSON.stringify(envelope.payload)}
|
|
|
8572
8824
|
|
|
8573
8825
|
// src/connectors-config.ts
|
|
8574
8826
|
import os4 from "os";
|
|
8575
|
-
import
|
|
8827
|
+
import path48 from "path";
|
|
8576
8828
|
import { promises as fs28 } from "fs";
|
|
8577
8829
|
var CONNECTORS_CONFIG_VERSION = 1;
|
|
8578
8830
|
var EnvRefUnsetError = class extends Error {
|
|
@@ -8587,11 +8839,11 @@ var EnvRefUnsetError = class extends Error {
|
|
|
8587
8839
|
};
|
|
8588
8840
|
function neatHome2() {
|
|
8589
8841
|
const override = process.env.NEAT_HOME;
|
|
8590
|
-
if (override && override.length > 0) return
|
|
8591
|
-
return
|
|
8842
|
+
if (override && override.length > 0) return path48.resolve(override);
|
|
8843
|
+
return path48.join(os4.homedir(), ".neat");
|
|
8592
8844
|
}
|
|
8593
8845
|
function connectorsConfigPath(home = neatHome2()) {
|
|
8594
|
-
return
|
|
8846
|
+
return path48.join(home, "connectors.json");
|
|
8595
8847
|
}
|
|
8596
8848
|
var MODE_MASK_LOOSER_THAN_0600 = 63;
|
|
8597
8849
|
async function warnIfModeLooserThan0600(file) {
|
|
@@ -8722,7 +8974,7 @@ function connectorMatchesProject(entry, project) {
|
|
|
8722
8974
|
var CONNECTORS_LOCK_TIMEOUT_MS = 5e3;
|
|
8723
8975
|
var CONNECTORS_LOCK_RETRY_MS = 50;
|
|
8724
8976
|
function connectorsConfigLockPath(home = neatHome2()) {
|
|
8725
|
-
return
|
|
8977
|
+
return path48.join(home, "connectors.json.lock");
|
|
8726
8978
|
}
|
|
8727
8979
|
function isEnvRef(value) {
|
|
8728
8980
|
return value.length > 1 && value.startsWith("$");
|
|
@@ -8735,7 +8987,7 @@ function redactCredentialRef(ref) {
|
|
|
8735
8987
|
return out;
|
|
8736
8988
|
}
|
|
8737
8989
|
async function writeConfigAtomically0600(file, contents) {
|
|
8738
|
-
await fs28.mkdir(
|
|
8990
|
+
await fs28.mkdir(path48.dirname(file), { recursive: true });
|
|
8739
8991
|
const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
8740
8992
|
const fd = await fs28.open(tmp, "w", 384);
|
|
8741
8993
|
try {
|
|
@@ -8749,7 +9001,7 @@ async function writeConfigAtomically0600(file, contents) {
|
|
|
8749
9001
|
}
|
|
8750
9002
|
async function acquireConnectorsLock(lockPath, timeoutMs = CONNECTORS_LOCK_TIMEOUT_MS) {
|
|
8751
9003
|
const deadline = Date.now() + timeoutMs;
|
|
8752
|
-
await fs28.mkdir(
|
|
9004
|
+
await fs28.mkdir(path48.dirname(lockPath), { recursive: true });
|
|
8753
9005
|
for (; ; ) {
|
|
8754
9006
|
try {
|
|
8755
9007
|
const fd = await fs28.open(lockPath, "wx");
|
|
@@ -9743,4 +9995,4 @@ export {
|
|
|
9743
9995
|
recordConnectorPoll,
|
|
9744
9996
|
buildApi
|
|
9745
9997
|
};
|
|
9746
|
-
//# sourceMappingURL=chunk-
|
|
9998
|
+
//# sourceMappingURL=chunk-ZZ3VUCWL.js.map
|