@neat.is/core 0.2.9 → 0.3.0
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-IRPH6KL4.js → chunk-B7UUGIXB.js} +3 -1
- package/dist/chunk-B7UUGIXB.js.map +1 -0
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/{chunk-BVF7MVR5.js → chunk-I5KODOJV.js} +2 -2
- package/dist/{chunk-5KX7EI4F.js → chunk-NVCEZXL7.js} +331 -20
- package/dist/chunk-NVCEZXL7.js.map +1 -0
- package/dist/{chunk-I5IMCXRO.js → chunk-QYUB3FQL.js} +2 -2
- package/dist/cli.cjs +443 -42
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +111 -13
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +323 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -12
- package/dist/index.d.ts +4 -12
- package/dist/index.js +5 -4
- package/dist/neatd.cjs +106 -5
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +112 -6
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-B4XBSI4W.js → otel-grpc-USFL3OI3.js} +3 -2
- package/dist/otel-grpc-USFL3OI3.js.map +1 -0
- package/dist/server.cjs +431 -125
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +4 -3
- package/dist/server.js.map +1 -1
- package/package.json +2 -2
- package/dist/chunk-5KX7EI4F.js.map +0 -1
- package/dist/chunk-IRPH6KL4.js.map +0 -1
- /package/dist/{otel-grpc-B4XBSI4W.js.map → chunk-DGUM43GV.js.map} +0 -0
- /package/dist/{chunk-BVF7MVR5.js.map → chunk-I5KODOJV.js.map} +0 -0
- /package/dist/{chunk-I5IMCXRO.js.map → chunk-QYUB3FQL.js.map} +0 -0
package/dist/index.cjs
CHANGED
|
@@ -3869,7 +3869,244 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
3869
3869
|
init_cjs_shims();
|
|
3870
3870
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
3871
3871
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
3872
|
-
var
|
|
3872
|
+
var import_types20 = require("@neat.is/types");
|
|
3873
|
+
|
|
3874
|
+
// src/divergences.ts
|
|
3875
|
+
init_cjs_shims();
|
|
3876
|
+
var import_types18 = require("@neat.is/types");
|
|
3877
|
+
function bucketKey(source, target, type) {
|
|
3878
|
+
return `${type}|${source}|${target}`;
|
|
3879
|
+
}
|
|
3880
|
+
function bucketEdges(graph) {
|
|
3881
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3882
|
+
graph.forEachEdge((id, attrs) => {
|
|
3883
|
+
const e = attrs;
|
|
3884
|
+
const parsed = (0, import_types18.parseEdgeId)(id);
|
|
3885
|
+
const provenance = parsed?.provenance ?? e.provenance;
|
|
3886
|
+
const key = bucketKey(e.source, e.target, e.type);
|
|
3887
|
+
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
3888
|
+
switch (provenance) {
|
|
3889
|
+
case import_types18.Provenance.EXTRACTED:
|
|
3890
|
+
cur.extracted = e;
|
|
3891
|
+
break;
|
|
3892
|
+
case import_types18.Provenance.OBSERVED:
|
|
3893
|
+
cur.observed = e;
|
|
3894
|
+
break;
|
|
3895
|
+
case import_types18.Provenance.INFERRED:
|
|
3896
|
+
cur.inferred = e;
|
|
3897
|
+
break;
|
|
3898
|
+
case import_types18.Provenance.FRONTIER:
|
|
3899
|
+
cur.frontier = e;
|
|
3900
|
+
break;
|
|
3901
|
+
default:
|
|
3902
|
+
if (e.provenance === import_types18.Provenance.STALE) cur.stale = e;
|
|
3903
|
+
}
|
|
3904
|
+
buckets.set(key, cur);
|
|
3905
|
+
});
|
|
3906
|
+
return buckets;
|
|
3907
|
+
}
|
|
3908
|
+
function nodeIsFrontier(graph, nodeId) {
|
|
3909
|
+
if (!graph.hasNode(nodeId)) return false;
|
|
3910
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
3911
|
+
return attrs.type === import_types18.NodeType.FrontierNode;
|
|
3912
|
+
}
|
|
3913
|
+
function hasAnyObservedFromSource(graph, sourceId) {
|
|
3914
|
+
if (!graph.hasNode(sourceId)) return false;
|
|
3915
|
+
for (const edgeId of graph.outboundEdges(sourceId)) {
|
|
3916
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
3917
|
+
if (e.provenance === import_types18.Provenance.OBSERVED) return true;
|
|
3918
|
+
}
|
|
3919
|
+
return false;
|
|
3920
|
+
}
|
|
3921
|
+
function clampConfidence(n) {
|
|
3922
|
+
if (!Number.isFinite(n)) return 0;
|
|
3923
|
+
return Math.max(0, Math.min(1, n));
|
|
3924
|
+
}
|
|
3925
|
+
function reasonForMissingObserved(source, target, type) {
|
|
3926
|
+
return `Code declares ${source} \u2192 ${target} (${type}) but no production traffic has been observed for this edge.`;
|
|
3927
|
+
}
|
|
3928
|
+
function reasonForMissingExtracted(source, target, type) {
|
|
3929
|
+
return `Production observed ${source} \u2192 ${target} (${type}) but static analysis did not surface this edge.`;
|
|
3930
|
+
}
|
|
3931
|
+
var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
|
|
3932
|
+
var RECOMMENDATION_MISSING_EXTRACTED = "Likely dynamic dispatch, reflection, or a coverage gap in tree-sitter extraction. Consider an `aliases` entry on the source service or file an extractor issue.";
|
|
3933
|
+
var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
|
|
3934
|
+
function detectMissingDivergences(graph, bucket) {
|
|
3935
|
+
const out = [];
|
|
3936
|
+
if (bucket.extracted && !bucket.observed) {
|
|
3937
|
+
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
3938
|
+
const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
|
|
3939
|
+
out.push({
|
|
3940
|
+
type: "missing-observed",
|
|
3941
|
+
source: bucket.source,
|
|
3942
|
+
target: bucket.target,
|
|
3943
|
+
edgeType: bucket.type,
|
|
3944
|
+
extracted: bucket.extracted,
|
|
3945
|
+
confidence: sourceHasTraffic ? 1 : 0.5,
|
|
3946
|
+
reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
|
|
3947
|
+
recommendation: RECOMMENDATION_MISSING_OBSERVED
|
|
3948
|
+
});
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
if (bucket.observed && !bucket.extracted) {
|
|
3952
|
+
const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
|
|
3953
|
+
out.push({
|
|
3954
|
+
type: "missing-extracted",
|
|
3955
|
+
source: bucket.source,
|
|
3956
|
+
target: bucket.target,
|
|
3957
|
+
edgeType: bucket.type,
|
|
3958
|
+
observed: bucket.observed,
|
|
3959
|
+
confidence: cascaded,
|
|
3960
|
+
reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
|
|
3961
|
+
recommendation: RECOMMENDATION_MISSING_EXTRACTED
|
|
3962
|
+
});
|
|
3963
|
+
}
|
|
3964
|
+
return out;
|
|
3965
|
+
}
|
|
3966
|
+
function declaredHostFor(svc) {
|
|
3967
|
+
const raw = svc.dbConnectionTarget?.trim();
|
|
3968
|
+
if (!raw) return null;
|
|
3969
|
+
const colon = raw.lastIndexOf(":");
|
|
3970
|
+
if (colon === -1) return raw;
|
|
3971
|
+
const port = raw.slice(colon + 1);
|
|
3972
|
+
if (/^\d+$/.test(port)) return raw.slice(0, colon);
|
|
3973
|
+
return raw;
|
|
3974
|
+
}
|
|
3975
|
+
function hasExtractedConfiguredBy(graph, svcId) {
|
|
3976
|
+
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
3977
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
3978
|
+
if (e.type === import_types18.EdgeType.CONFIGURED_BY && e.provenance === import_types18.Provenance.EXTRACTED) {
|
|
3979
|
+
return true;
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
return false;
|
|
3983
|
+
}
|
|
3984
|
+
function detectHostMismatch(graph, svcId, svc) {
|
|
3985
|
+
const declaredHost = declaredHostFor(svc);
|
|
3986
|
+
if (!declaredHost) return [];
|
|
3987
|
+
if (!hasExtractedConfiguredBy(graph, svcId)) return [];
|
|
3988
|
+
const out = [];
|
|
3989
|
+
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
3990
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
3991
|
+
if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
|
|
3992
|
+
if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
|
|
3993
|
+
const target = graph.getNodeAttributes(edge.target);
|
|
3994
|
+
if (target.type !== import_types18.NodeType.DatabaseNode) continue;
|
|
3995
|
+
const observedHost = target.host?.trim();
|
|
3996
|
+
if (!observedHost) continue;
|
|
3997
|
+
if (observedHost === declaredHost) continue;
|
|
3998
|
+
out.push({
|
|
3999
|
+
type: "host-mismatch",
|
|
4000
|
+
source: svcId,
|
|
4001
|
+
target: edge.target,
|
|
4002
|
+
extractedHost: declaredHost,
|
|
4003
|
+
observedHost,
|
|
4004
|
+
confidence: clampConfidence(confidenceForEdge(edge)),
|
|
4005
|
+
reason: `Config declares ${svcId} connects to ${declaredHost}; production connects to ${observedHost}.`,
|
|
4006
|
+
recommendation: RECOMMENDATION_HOST_MISMATCH
|
|
4007
|
+
});
|
|
4008
|
+
}
|
|
4009
|
+
return out;
|
|
4010
|
+
}
|
|
4011
|
+
function detectCompatDivergences(graph, svcId, svc) {
|
|
4012
|
+
const out = [];
|
|
4013
|
+
const deps = svc.dependencies ?? {};
|
|
4014
|
+
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4015
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
4016
|
+
if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
|
|
4017
|
+
if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
|
|
4018
|
+
const target = graph.getNodeAttributes(edge.target);
|
|
4019
|
+
if (target.type !== import_types18.NodeType.DatabaseNode) continue;
|
|
4020
|
+
for (const pair of compatPairs()) {
|
|
4021
|
+
if (pair.engine !== target.engine) continue;
|
|
4022
|
+
const declared = deps[pair.driver];
|
|
4023
|
+
if (!declared) continue;
|
|
4024
|
+
const result = checkCompatibility(
|
|
4025
|
+
pair.driver,
|
|
4026
|
+
declared,
|
|
4027
|
+
target.engine,
|
|
4028
|
+
target.engineVersion
|
|
4029
|
+
);
|
|
4030
|
+
if (!result.compatible && result.reason) {
|
|
4031
|
+
out.push({
|
|
4032
|
+
type: "version-mismatch",
|
|
4033
|
+
source: svcId,
|
|
4034
|
+
target: edge.target,
|
|
4035
|
+
extractedVersion: declared,
|
|
4036
|
+
observedVersion: target.engineVersion,
|
|
4037
|
+
compatibility: "incompatible",
|
|
4038
|
+
confidence: 1,
|
|
4039
|
+
reason: result.reason,
|
|
4040
|
+
recommendation: result.minDriverVersion ? `Upgrade ${pair.driver} to >= ${result.minDriverVersion}.` : `Update the ${pair.driver} driver to a version compatible with ${target.engine} ${target.engineVersion}.`
|
|
4041
|
+
});
|
|
4042
|
+
}
|
|
4043
|
+
}
|
|
4044
|
+
for (const rule of deprecatedApis()) {
|
|
4045
|
+
const declared = deps[rule.package];
|
|
4046
|
+
if (!declared) continue;
|
|
4047
|
+
const result = checkDeprecatedApi(rule, declared);
|
|
4048
|
+
if (!result.compatible && result.reason) {
|
|
4049
|
+
const ruleRef = {
|
|
4050
|
+
kind: rule.kind ?? "deprecated-api",
|
|
4051
|
+
reason: result.reason,
|
|
4052
|
+
package: rule.package
|
|
4053
|
+
};
|
|
4054
|
+
out.push({
|
|
4055
|
+
type: "compat-violation",
|
|
4056
|
+
source: svcId,
|
|
4057
|
+
target: edge.target,
|
|
4058
|
+
rule: ruleRef,
|
|
4059
|
+
observed: edge,
|
|
4060
|
+
confidence: 1,
|
|
4061
|
+
reason: result.reason,
|
|
4062
|
+
recommendation: `Replace deprecated ${rule.package}@${declared} with a supported version.`
|
|
4063
|
+
});
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
return out;
|
|
4068
|
+
}
|
|
4069
|
+
function involvesNode(d, nodeId) {
|
|
4070
|
+
return d.source === nodeId || d.target === nodeId;
|
|
4071
|
+
}
|
|
4072
|
+
function computeDivergences(graph, opts = {}) {
|
|
4073
|
+
const all = [];
|
|
4074
|
+
const buckets = bucketEdges(graph);
|
|
4075
|
+
for (const bucket of buckets.values()) {
|
|
4076
|
+
for (const d of detectMissingDivergences(graph, bucket)) all.push(d);
|
|
4077
|
+
}
|
|
4078
|
+
graph.forEachNode((nodeId, attrs) => {
|
|
4079
|
+
const n = attrs;
|
|
4080
|
+
if (n.type !== import_types18.NodeType.ServiceNode) return;
|
|
4081
|
+
const svc = n;
|
|
4082
|
+
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4083
|
+
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
4084
|
+
});
|
|
4085
|
+
let filtered = all;
|
|
4086
|
+
if (opts.type) {
|
|
4087
|
+
const allowed = opts.type;
|
|
4088
|
+
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
4089
|
+
}
|
|
4090
|
+
if (opts.minConfidence !== void 0) {
|
|
4091
|
+
const threshold = opts.minConfidence;
|
|
4092
|
+
filtered = filtered.filter((d) => d.confidence >= threshold);
|
|
4093
|
+
}
|
|
4094
|
+
if (opts.node) {
|
|
4095
|
+
const target = opts.node;
|
|
4096
|
+
filtered = filtered.filter((d) => involvesNode(d, target));
|
|
4097
|
+
}
|
|
4098
|
+
filtered.sort((a, b) => {
|
|
4099
|
+
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
4100
|
+
if (a.type !== b.type) return a.type.localeCompare(b.type);
|
|
4101
|
+
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4102
|
+
return a.target.localeCompare(b.target);
|
|
4103
|
+
});
|
|
4104
|
+
return import_types18.DivergenceResultSchema.parse({
|
|
4105
|
+
divergences: filtered,
|
|
4106
|
+
totalAffected: filtered.length,
|
|
4107
|
+
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4108
|
+
});
|
|
4109
|
+
}
|
|
3873
4110
|
|
|
3874
4111
|
// src/diff.ts
|
|
3875
4112
|
init_cjs_shims();
|
|
@@ -4005,7 +4242,7 @@ init_cjs_shims();
|
|
|
4005
4242
|
var import_node_fs18 = require("fs");
|
|
4006
4243
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4007
4244
|
var import_node_path30 = __toESM(require("path"), 1);
|
|
4008
|
-
var
|
|
4245
|
+
var import_types19 = require("@neat.is/types");
|
|
4009
4246
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
4010
4247
|
var LOCK_RETRY_MS = 50;
|
|
4011
4248
|
function neatHome() {
|
|
@@ -4084,10 +4321,10 @@ async function readRegistry() {
|
|
|
4084
4321
|
throw err;
|
|
4085
4322
|
}
|
|
4086
4323
|
const parsed = JSON.parse(raw);
|
|
4087
|
-
return
|
|
4324
|
+
return import_types19.RegistryFileSchema.parse(parsed);
|
|
4088
4325
|
}
|
|
4089
4326
|
async function writeRegistry(reg) {
|
|
4090
|
-
const validated =
|
|
4327
|
+
const validated = import_types19.RegistryFileSchema.parse(reg);
|
|
4091
4328
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4092
4329
|
}
|
|
4093
4330
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -4279,9 +4516,15 @@ function registerRoutes(scope, ctx) {
|
|
|
4279
4516
|
scope.get("/health", async (req, reply) => {
|
|
4280
4517
|
const proj = resolveProject(registry, req, reply);
|
|
4281
4518
|
if (!proj) return;
|
|
4519
|
+
const uptimeMs = Date.now() - startedAt;
|
|
4282
4520
|
return {
|
|
4283
|
-
|
|
4521
|
+
ok: true,
|
|
4284
4522
|
project: proj.name,
|
|
4523
|
+
uptimeMs,
|
|
4524
|
+
// Legacy fields kept additively. The web shell's StatusBar reads
|
|
4525
|
+
// nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
|
|
4526
|
+
// the canonical triple and lets the extras pass through.
|
|
4527
|
+
uptime: Math.floor(uptimeMs / 1e3),
|
|
4285
4528
|
nodeCount: proj.graph.order,
|
|
4286
4529
|
edgeCount: proj.graph.size,
|
|
4287
4530
|
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -4301,7 +4544,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4301
4544
|
if (!proj.graph.hasNode(id)) {
|
|
4302
4545
|
return reply.code(404).send({ error: "node not found", id });
|
|
4303
4546
|
}
|
|
4304
|
-
return proj.graph.getNodeAttributes(id);
|
|
4547
|
+
return { node: proj.graph.getNodeAttributes(id) };
|
|
4305
4548
|
}
|
|
4306
4549
|
);
|
|
4307
4550
|
scope.get(
|
|
@@ -4318,12 +4561,12 @@ function registerRoutes(scope, ctx) {
|
|
|
4318
4561
|
return { inbound, outbound };
|
|
4319
4562
|
}
|
|
4320
4563
|
);
|
|
4321
|
-
scope.get("/graph/
|
|
4564
|
+
scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
|
|
4322
4565
|
const proj = resolveProject(registry, req, reply);
|
|
4323
4566
|
if (!proj) return;
|
|
4324
|
-
const {
|
|
4325
|
-
if (!proj.graph.hasNode(
|
|
4326
|
-
return reply.code(404).send({ error: "node not found", id });
|
|
4567
|
+
const { nodeId } = req.params;
|
|
4568
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
4569
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
4327
4570
|
}
|
|
4328
4571
|
const depth = req.query.depth ? Number(req.query.depth) : TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH;
|
|
4329
4572
|
if (!Number.isFinite(depth) || depth < 1 || depth > TRANSITIVE_DEPENDENCIES_MAX_DEPTH) {
|
|
@@ -4331,25 +4574,67 @@ function registerRoutes(scope, ctx) {
|
|
|
4331
4574
|
error: `depth must be an integer in [1, ${TRANSITIVE_DEPENDENCIES_MAX_DEPTH}]`
|
|
4332
4575
|
});
|
|
4333
4576
|
}
|
|
4334
|
-
return getTransitiveDependencies(proj.graph,
|
|
4577
|
+
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
4578
|
+
});
|
|
4579
|
+
scope.get("/graph/divergences", async (req, reply) => {
|
|
4580
|
+
const proj = resolveProject(registry, req, reply);
|
|
4581
|
+
if (!proj) return;
|
|
4582
|
+
let typeFilter;
|
|
4583
|
+
if (req.query.type) {
|
|
4584
|
+
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4585
|
+
const parsed = [];
|
|
4586
|
+
for (const c of candidates) {
|
|
4587
|
+
const r = import_types20.DivergenceTypeSchema.safeParse(c);
|
|
4588
|
+
if (!r.success) {
|
|
4589
|
+
return reply.code(400).send({
|
|
4590
|
+
error: `unknown divergence type "${c}"`,
|
|
4591
|
+
allowed: import_types20.DivergenceTypeSchema.options
|
|
4592
|
+
});
|
|
4593
|
+
}
|
|
4594
|
+
parsed.push(r.data);
|
|
4595
|
+
}
|
|
4596
|
+
typeFilter = new Set(parsed);
|
|
4597
|
+
}
|
|
4598
|
+
let minConfidence;
|
|
4599
|
+
if (req.query.minConfidence !== void 0) {
|
|
4600
|
+
const n = Number(req.query.minConfidence);
|
|
4601
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
4602
|
+
return reply.code(400).send({
|
|
4603
|
+
error: "minConfidence must be a number in [0, 1]"
|
|
4604
|
+
});
|
|
4605
|
+
}
|
|
4606
|
+
minConfidence = n;
|
|
4607
|
+
}
|
|
4608
|
+
return computeDivergences(proj.graph, {
|
|
4609
|
+
...typeFilter ? { type: typeFilter } : {},
|
|
4610
|
+
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
4611
|
+
...req.query.node ? { node: req.query.node } : {}
|
|
4612
|
+
});
|
|
4335
4613
|
});
|
|
4336
4614
|
scope.get("/incidents", async (req, reply) => {
|
|
4337
4615
|
const proj = resolveProject(registry, req, reply);
|
|
4338
4616
|
if (!proj) return;
|
|
4339
4617
|
const epath = errorsPathFor(proj);
|
|
4340
|
-
if (!epath) return [];
|
|
4341
|
-
|
|
4618
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
4619
|
+
const events = await readErrorEvents(epath);
|
|
4620
|
+
const total = events.length;
|
|
4621
|
+
const limit = req.query.limit ? Number(req.query.limit) : 50;
|
|
4622
|
+
const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
|
|
4623
|
+
const sliced = events.slice(0, safeLimit);
|
|
4624
|
+
return { count: sliced.length, total, events: sliced };
|
|
4342
4625
|
});
|
|
4343
|
-
scope.get("/
|
|
4626
|
+
scope.get("/stale-events", async (req, reply) => {
|
|
4344
4627
|
const proj = resolveProject(registry, req, reply);
|
|
4345
4628
|
if (!proj) return;
|
|
4346
4629
|
const spath = staleEventsPathFor(proj);
|
|
4347
|
-
if (!spath) return [];
|
|
4630
|
+
if (!spath) return { count: 0, total: 0, events: [] };
|
|
4348
4631
|
const events = await readStaleEvents(spath);
|
|
4349
4632
|
const filtered = req.query.edgeType ? events.filter((e) => e.edgeType === req.query.edgeType) : events;
|
|
4350
4633
|
const ordered = [...filtered].reverse();
|
|
4634
|
+
const total = ordered.length;
|
|
4351
4635
|
const limit = req.query.limit ? Number(req.query.limit) : 50;
|
|
4352
|
-
|
|
4636
|
+
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
4637
|
+
return { count: sliced.length, total, events: sliced };
|
|
4353
4638
|
});
|
|
4354
4639
|
scope.get(
|
|
4355
4640
|
"/incidents/:nodeId",
|
|
@@ -4361,14 +4646,15 @@ function registerRoutes(scope, ctx) {
|
|
|
4361
4646
|
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
4362
4647
|
}
|
|
4363
4648
|
const epath = errorsPathFor(proj);
|
|
4364
|
-
if (!epath) return [];
|
|
4649
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
4365
4650
|
const events = await readErrorEvents(epath);
|
|
4366
|
-
|
|
4651
|
+
const filtered = events.filter(
|
|
4367
4652
|
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
4368
4653
|
);
|
|
4654
|
+
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
4369
4655
|
}
|
|
4370
4656
|
);
|
|
4371
|
-
scope.get("/
|
|
4657
|
+
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
4372
4658
|
const proj = resolveProject(registry, req, reply);
|
|
4373
4659
|
if (!proj) return;
|
|
4374
4660
|
const { nodeId } = req.params;
|
|
@@ -4388,7 +4674,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4388
4674
|
if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
|
|
4389
4675
|
return result;
|
|
4390
4676
|
});
|
|
4391
|
-
scope.get("/
|
|
4677
|
+
scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
|
|
4392
4678
|
const proj = resolveProject(registry, req, reply);
|
|
4393
4679
|
if (!proj) return;
|
|
4394
4680
|
const { nodeId } = req.params;
|
|
@@ -4486,7 +4772,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4486
4772
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4487
4773
|
let violations = await log.readAll();
|
|
4488
4774
|
if (req.query.severity) {
|
|
4489
|
-
const sev =
|
|
4775
|
+
const sev = import_types20.PolicySeveritySchema.safeParse(req.query.severity);
|
|
4490
4776
|
if (!sev.success) {
|
|
4491
4777
|
return reply.code(400).send({
|
|
4492
4778
|
error: "invalid severity",
|
|
@@ -4498,12 +4784,12 @@ function registerRoutes(scope, ctx) {
|
|
|
4498
4784
|
if (req.query.policyId) {
|
|
4499
4785
|
violations = violations.filter((v) => v.policyId === req.query.policyId);
|
|
4500
4786
|
}
|
|
4501
|
-
return violations;
|
|
4787
|
+
return { violations };
|
|
4502
4788
|
});
|
|
4503
4789
|
scope.post("/policies/check", async (req, reply) => {
|
|
4504
4790
|
const proj = resolveProject(registry, req, reply);
|
|
4505
4791
|
if (!proj) return;
|
|
4506
|
-
const parsed =
|
|
4792
|
+
const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4507
4793
|
if (!parsed.success) {
|
|
4508
4794
|
return reply.code(400).send({
|
|
4509
4795
|
error: "invalid /policies/check body",
|
|
@@ -4577,6 +4863,20 @@ async function buildApi(opts) {
|
|
|
4577
4863
|
});
|
|
4578
4864
|
}
|
|
4579
4865
|
});
|
|
4866
|
+
app.get("/projects/:project", async (req, reply) => {
|
|
4867
|
+
try {
|
|
4868
|
+
const entry = await getProject(req.params.project);
|
|
4869
|
+
if (!entry) {
|
|
4870
|
+
return reply.code(404).send({ error: "project not found", project: req.params.project });
|
|
4871
|
+
}
|
|
4872
|
+
return { project: entry };
|
|
4873
|
+
} catch (err) {
|
|
4874
|
+
return reply.code(500).send({
|
|
4875
|
+
error: "failed to read project registry",
|
|
4876
|
+
details: err.message
|
|
4877
|
+
});
|
|
4878
|
+
}
|
|
4879
|
+
});
|
|
4580
4880
|
registerRoutes(app, routeCtx);
|
|
4581
4881
|
await app.register(
|
|
4582
4882
|
async (scope) => {
|