@neat.is/core 0.2.9 → 0.2.10
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-BVF7MVR5.js → chunk-5BQWBQJR.js} +2 -2
- package/dist/chunk-DGUM43GV.js +11 -0
- package/dist/{chunk-IRPH6KL4.js → chunk-JMLZOKCS.js} +3 -1
- package/dist/{chunk-I5IMCXRO.js → chunk-QYUB3FQL.js} +2 -2
- package/dist/{chunk-5KX7EI4F.js → chunk-W7ZYJZC7.js} +285 -3
- package/dist/chunk-W7ZYJZC7.js.map +1 -0
- package/dist/cli.cjs +383 -17
- 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 +100 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +278 -6
- package/dist/index.cjs.map +1 -1
- 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 +382 -108
- 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-BVF7MVR5.js.map → chunk-5BQWBQJR.js.map} +0 -0
- /package/dist/{otel-grpc-B4XBSI4W.js.map → chunk-DGUM43GV.js.map} +0 -0
- /package/dist/{chunk-IRPH6KL4.js.map → chunk-JMLZOKCS.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 {
|
|
@@ -4333,6 +4570,41 @@ function registerRoutes(scope, ctx) {
|
|
|
4333
4570
|
}
|
|
4334
4571
|
return getTransitiveDependencies(proj.graph, id, depth);
|
|
4335
4572
|
});
|
|
4573
|
+
scope.get("/graph/divergences", async (req, reply) => {
|
|
4574
|
+
const proj = resolveProject(registry, req, reply);
|
|
4575
|
+
if (!proj) return;
|
|
4576
|
+
let typeFilter;
|
|
4577
|
+
if (req.query.type) {
|
|
4578
|
+
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4579
|
+
const parsed = [];
|
|
4580
|
+
for (const c of candidates) {
|
|
4581
|
+
const r = import_types20.DivergenceTypeSchema.safeParse(c);
|
|
4582
|
+
if (!r.success) {
|
|
4583
|
+
return reply.code(400).send({
|
|
4584
|
+
error: `unknown divergence type "${c}"`,
|
|
4585
|
+
allowed: import_types20.DivergenceTypeSchema.options
|
|
4586
|
+
});
|
|
4587
|
+
}
|
|
4588
|
+
parsed.push(r.data);
|
|
4589
|
+
}
|
|
4590
|
+
typeFilter = new Set(parsed);
|
|
4591
|
+
}
|
|
4592
|
+
let minConfidence;
|
|
4593
|
+
if (req.query.minConfidence !== void 0) {
|
|
4594
|
+
const n = Number(req.query.minConfidence);
|
|
4595
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
4596
|
+
return reply.code(400).send({
|
|
4597
|
+
error: "minConfidence must be a number in [0, 1]"
|
|
4598
|
+
});
|
|
4599
|
+
}
|
|
4600
|
+
minConfidence = n;
|
|
4601
|
+
}
|
|
4602
|
+
return computeDivergences(proj.graph, {
|
|
4603
|
+
...typeFilter ? { type: typeFilter } : {},
|
|
4604
|
+
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
4605
|
+
...req.query.node ? { node: req.query.node } : {}
|
|
4606
|
+
});
|
|
4607
|
+
});
|
|
4336
4608
|
scope.get("/incidents", async (req, reply) => {
|
|
4337
4609
|
const proj = resolveProject(registry, req, reply);
|
|
4338
4610
|
if (!proj) return;
|
|
@@ -4486,7 +4758,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4486
4758
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4487
4759
|
let violations = await log.readAll();
|
|
4488
4760
|
if (req.query.severity) {
|
|
4489
|
-
const sev =
|
|
4761
|
+
const sev = import_types20.PolicySeveritySchema.safeParse(req.query.severity);
|
|
4490
4762
|
if (!sev.success) {
|
|
4491
4763
|
return reply.code(400).send({
|
|
4492
4764
|
error: "invalid severity",
|
|
@@ -4503,7 +4775,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4503
4775
|
scope.post("/policies/check", async (req, reply) => {
|
|
4504
4776
|
const proj = resolveProject(registry, req, reply);
|
|
4505
4777
|
if (!proj) return;
|
|
4506
|
-
const parsed =
|
|
4778
|
+
const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4507
4779
|
if (!parsed.success) {
|
|
4508
4780
|
return reply.code(400).send({
|
|
4509
4781
|
error: "invalid /policies/check body",
|