@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/cli.cjs
CHANGED
|
@@ -3908,7 +3908,244 @@ var import_chokidar = __toESM(require("chokidar"), 1);
|
|
|
3908
3908
|
init_cjs_shims();
|
|
3909
3909
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
3910
3910
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
3911
|
-
var
|
|
3911
|
+
var import_types20 = require("@neat.is/types");
|
|
3912
|
+
|
|
3913
|
+
// src/divergences.ts
|
|
3914
|
+
init_cjs_shims();
|
|
3915
|
+
var import_types18 = require("@neat.is/types");
|
|
3916
|
+
function bucketKey(source, target, type) {
|
|
3917
|
+
return `${type}|${source}|${target}`;
|
|
3918
|
+
}
|
|
3919
|
+
function bucketEdges(graph) {
|
|
3920
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
3921
|
+
graph.forEachEdge((id, attrs) => {
|
|
3922
|
+
const e = attrs;
|
|
3923
|
+
const parsed = (0, import_types18.parseEdgeId)(id);
|
|
3924
|
+
const provenance = parsed?.provenance ?? e.provenance;
|
|
3925
|
+
const key = bucketKey(e.source, e.target, e.type);
|
|
3926
|
+
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
3927
|
+
switch (provenance) {
|
|
3928
|
+
case import_types18.Provenance.EXTRACTED:
|
|
3929
|
+
cur.extracted = e;
|
|
3930
|
+
break;
|
|
3931
|
+
case import_types18.Provenance.OBSERVED:
|
|
3932
|
+
cur.observed = e;
|
|
3933
|
+
break;
|
|
3934
|
+
case import_types18.Provenance.INFERRED:
|
|
3935
|
+
cur.inferred = e;
|
|
3936
|
+
break;
|
|
3937
|
+
case import_types18.Provenance.FRONTIER:
|
|
3938
|
+
cur.frontier = e;
|
|
3939
|
+
break;
|
|
3940
|
+
default:
|
|
3941
|
+
if (e.provenance === import_types18.Provenance.STALE) cur.stale = e;
|
|
3942
|
+
}
|
|
3943
|
+
buckets.set(key, cur);
|
|
3944
|
+
});
|
|
3945
|
+
return buckets;
|
|
3946
|
+
}
|
|
3947
|
+
function nodeIsFrontier(graph, nodeId) {
|
|
3948
|
+
if (!graph.hasNode(nodeId)) return false;
|
|
3949
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
3950
|
+
return attrs.type === import_types18.NodeType.FrontierNode;
|
|
3951
|
+
}
|
|
3952
|
+
function hasAnyObservedFromSource(graph, sourceId) {
|
|
3953
|
+
if (!graph.hasNode(sourceId)) return false;
|
|
3954
|
+
for (const edgeId of graph.outboundEdges(sourceId)) {
|
|
3955
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
3956
|
+
if (e.provenance === import_types18.Provenance.OBSERVED) return true;
|
|
3957
|
+
}
|
|
3958
|
+
return false;
|
|
3959
|
+
}
|
|
3960
|
+
function clampConfidence(n) {
|
|
3961
|
+
if (!Number.isFinite(n)) return 0;
|
|
3962
|
+
return Math.max(0, Math.min(1, n));
|
|
3963
|
+
}
|
|
3964
|
+
function reasonForMissingObserved(source, target, type) {
|
|
3965
|
+
return `Code declares ${source} \u2192 ${target} (${type}) but no production traffic has been observed for this edge.`;
|
|
3966
|
+
}
|
|
3967
|
+
function reasonForMissingExtracted(source, target, type) {
|
|
3968
|
+
return `Production observed ${source} \u2192 ${target} (${type}) but static analysis did not surface this edge.`;
|
|
3969
|
+
}
|
|
3970
|
+
var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
|
|
3971
|
+
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.";
|
|
3972
|
+
var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
|
|
3973
|
+
function detectMissingDivergences(graph, bucket) {
|
|
3974
|
+
const out = [];
|
|
3975
|
+
if (bucket.extracted && !bucket.observed) {
|
|
3976
|
+
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
3977
|
+
const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
|
|
3978
|
+
out.push({
|
|
3979
|
+
type: "missing-observed",
|
|
3980
|
+
source: bucket.source,
|
|
3981
|
+
target: bucket.target,
|
|
3982
|
+
edgeType: bucket.type,
|
|
3983
|
+
extracted: bucket.extracted,
|
|
3984
|
+
confidence: sourceHasTraffic ? 1 : 0.5,
|
|
3985
|
+
reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
|
|
3986
|
+
recommendation: RECOMMENDATION_MISSING_OBSERVED
|
|
3987
|
+
});
|
|
3988
|
+
}
|
|
3989
|
+
}
|
|
3990
|
+
if (bucket.observed && !bucket.extracted) {
|
|
3991
|
+
const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
|
|
3992
|
+
out.push({
|
|
3993
|
+
type: "missing-extracted",
|
|
3994
|
+
source: bucket.source,
|
|
3995
|
+
target: bucket.target,
|
|
3996
|
+
edgeType: bucket.type,
|
|
3997
|
+
observed: bucket.observed,
|
|
3998
|
+
confidence: cascaded,
|
|
3999
|
+
reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
|
|
4000
|
+
recommendation: RECOMMENDATION_MISSING_EXTRACTED
|
|
4001
|
+
});
|
|
4002
|
+
}
|
|
4003
|
+
return out;
|
|
4004
|
+
}
|
|
4005
|
+
function declaredHostFor(svc) {
|
|
4006
|
+
const raw = svc.dbConnectionTarget?.trim();
|
|
4007
|
+
if (!raw) return null;
|
|
4008
|
+
const colon = raw.lastIndexOf(":");
|
|
4009
|
+
if (colon === -1) return raw;
|
|
4010
|
+
const port = raw.slice(colon + 1);
|
|
4011
|
+
if (/^\d+$/.test(port)) return raw.slice(0, colon);
|
|
4012
|
+
return raw;
|
|
4013
|
+
}
|
|
4014
|
+
function hasExtractedConfiguredBy(graph, svcId) {
|
|
4015
|
+
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4016
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
4017
|
+
if (e.type === import_types18.EdgeType.CONFIGURED_BY && e.provenance === import_types18.Provenance.EXTRACTED) {
|
|
4018
|
+
return true;
|
|
4019
|
+
}
|
|
4020
|
+
}
|
|
4021
|
+
return false;
|
|
4022
|
+
}
|
|
4023
|
+
function detectHostMismatch(graph, svcId, svc) {
|
|
4024
|
+
const declaredHost = declaredHostFor(svc);
|
|
4025
|
+
if (!declaredHost) return [];
|
|
4026
|
+
if (!hasExtractedConfiguredBy(graph, svcId)) return [];
|
|
4027
|
+
const out = [];
|
|
4028
|
+
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4029
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
4030
|
+
if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
|
|
4031
|
+
if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
|
|
4032
|
+
const target = graph.getNodeAttributes(edge.target);
|
|
4033
|
+
if (target.type !== import_types18.NodeType.DatabaseNode) continue;
|
|
4034
|
+
const observedHost = target.host?.trim();
|
|
4035
|
+
if (!observedHost) continue;
|
|
4036
|
+
if (observedHost === declaredHost) continue;
|
|
4037
|
+
out.push({
|
|
4038
|
+
type: "host-mismatch",
|
|
4039
|
+
source: svcId,
|
|
4040
|
+
target: edge.target,
|
|
4041
|
+
extractedHost: declaredHost,
|
|
4042
|
+
observedHost,
|
|
4043
|
+
confidence: clampConfidence(confidenceForEdge(edge)),
|
|
4044
|
+
reason: `Config declares ${svcId} connects to ${declaredHost}; production connects to ${observedHost}.`,
|
|
4045
|
+
recommendation: RECOMMENDATION_HOST_MISMATCH
|
|
4046
|
+
});
|
|
4047
|
+
}
|
|
4048
|
+
return out;
|
|
4049
|
+
}
|
|
4050
|
+
function detectCompatDivergences(graph, svcId, svc) {
|
|
4051
|
+
const out = [];
|
|
4052
|
+
const deps = svc.dependencies ?? {};
|
|
4053
|
+
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4054
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
4055
|
+
if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
|
|
4056
|
+
if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
|
|
4057
|
+
const target = graph.getNodeAttributes(edge.target);
|
|
4058
|
+
if (target.type !== import_types18.NodeType.DatabaseNode) continue;
|
|
4059
|
+
for (const pair of compatPairs()) {
|
|
4060
|
+
if (pair.engine !== target.engine) continue;
|
|
4061
|
+
const declared = deps[pair.driver];
|
|
4062
|
+
if (!declared) continue;
|
|
4063
|
+
const result = checkCompatibility(
|
|
4064
|
+
pair.driver,
|
|
4065
|
+
declared,
|
|
4066
|
+
target.engine,
|
|
4067
|
+
target.engineVersion
|
|
4068
|
+
);
|
|
4069
|
+
if (!result.compatible && result.reason) {
|
|
4070
|
+
out.push({
|
|
4071
|
+
type: "version-mismatch",
|
|
4072
|
+
source: svcId,
|
|
4073
|
+
target: edge.target,
|
|
4074
|
+
extractedVersion: declared,
|
|
4075
|
+
observedVersion: target.engineVersion,
|
|
4076
|
+
compatibility: "incompatible",
|
|
4077
|
+
confidence: 1,
|
|
4078
|
+
reason: result.reason,
|
|
4079
|
+
recommendation: result.minDriverVersion ? `Upgrade ${pair.driver} to >= ${result.minDriverVersion}.` : `Update the ${pair.driver} driver to a version compatible with ${target.engine} ${target.engineVersion}.`
|
|
4080
|
+
});
|
|
4081
|
+
}
|
|
4082
|
+
}
|
|
4083
|
+
for (const rule of deprecatedApis()) {
|
|
4084
|
+
const declared = deps[rule.package];
|
|
4085
|
+
if (!declared) continue;
|
|
4086
|
+
const result = checkDeprecatedApi(rule, declared);
|
|
4087
|
+
if (!result.compatible && result.reason) {
|
|
4088
|
+
const ruleRef = {
|
|
4089
|
+
kind: rule.kind ?? "deprecated-api",
|
|
4090
|
+
reason: result.reason,
|
|
4091
|
+
package: rule.package
|
|
4092
|
+
};
|
|
4093
|
+
out.push({
|
|
4094
|
+
type: "compat-violation",
|
|
4095
|
+
source: svcId,
|
|
4096
|
+
target: edge.target,
|
|
4097
|
+
rule: ruleRef,
|
|
4098
|
+
observed: edge,
|
|
4099
|
+
confidence: 1,
|
|
4100
|
+
reason: result.reason,
|
|
4101
|
+
recommendation: `Replace deprecated ${rule.package}@${declared} with a supported version.`
|
|
4102
|
+
});
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
return out;
|
|
4107
|
+
}
|
|
4108
|
+
function involvesNode(d, nodeId) {
|
|
4109
|
+
return d.source === nodeId || d.target === nodeId;
|
|
4110
|
+
}
|
|
4111
|
+
function computeDivergences(graph, opts = {}) {
|
|
4112
|
+
const all = [];
|
|
4113
|
+
const buckets = bucketEdges(graph);
|
|
4114
|
+
for (const bucket of buckets.values()) {
|
|
4115
|
+
for (const d of detectMissingDivergences(graph, bucket)) all.push(d);
|
|
4116
|
+
}
|
|
4117
|
+
graph.forEachNode((nodeId, attrs) => {
|
|
4118
|
+
const n = attrs;
|
|
4119
|
+
if (n.type !== import_types18.NodeType.ServiceNode) return;
|
|
4120
|
+
const svc = n;
|
|
4121
|
+
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4122
|
+
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
4123
|
+
});
|
|
4124
|
+
let filtered = all;
|
|
4125
|
+
if (opts.type) {
|
|
4126
|
+
const allowed = opts.type;
|
|
4127
|
+
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
4128
|
+
}
|
|
4129
|
+
if (opts.minConfidence !== void 0) {
|
|
4130
|
+
const threshold = opts.minConfidence;
|
|
4131
|
+
filtered = filtered.filter((d) => d.confidence >= threshold);
|
|
4132
|
+
}
|
|
4133
|
+
if (opts.node) {
|
|
4134
|
+
const target = opts.node;
|
|
4135
|
+
filtered = filtered.filter((d) => involvesNode(d, target));
|
|
4136
|
+
}
|
|
4137
|
+
filtered.sort((a, b) => {
|
|
4138
|
+
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
4139
|
+
if (a.type !== b.type) return a.type.localeCompare(b.type);
|
|
4140
|
+
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4141
|
+
return a.target.localeCompare(b.target);
|
|
4142
|
+
});
|
|
4143
|
+
return import_types18.DivergenceResultSchema.parse({
|
|
4144
|
+
divergences: filtered,
|
|
4145
|
+
totalAffected: filtered.length,
|
|
4146
|
+
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4147
|
+
});
|
|
4148
|
+
}
|
|
3912
4149
|
|
|
3913
4150
|
// src/diff.ts
|
|
3914
4151
|
init_cjs_shims();
|
|
@@ -4044,7 +4281,7 @@ init_cjs_shims();
|
|
|
4044
4281
|
var import_node_fs18 = require("fs");
|
|
4045
4282
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
4046
4283
|
var import_node_path30 = __toESM(require("path"), 1);
|
|
4047
|
-
var
|
|
4284
|
+
var import_types19 = require("@neat.is/types");
|
|
4048
4285
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
4049
4286
|
var LOCK_RETRY_MS = 50;
|
|
4050
4287
|
function neatHome() {
|
|
@@ -4123,10 +4360,10 @@ async function readRegistry() {
|
|
|
4123
4360
|
throw err;
|
|
4124
4361
|
}
|
|
4125
4362
|
const parsed = JSON.parse(raw);
|
|
4126
|
-
return
|
|
4363
|
+
return import_types19.RegistryFileSchema.parse(parsed);
|
|
4127
4364
|
}
|
|
4128
4365
|
async function writeRegistry(reg) {
|
|
4129
|
-
const validated =
|
|
4366
|
+
const validated = import_types19.RegistryFileSchema.parse(reg);
|
|
4130
4367
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4131
4368
|
}
|
|
4132
4369
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -4359,6 +4596,41 @@ function registerRoutes(scope, ctx) {
|
|
|
4359
4596
|
}
|
|
4360
4597
|
return getTransitiveDependencies(proj.graph, id, depth);
|
|
4361
4598
|
});
|
|
4599
|
+
scope.get("/graph/divergences", async (req, reply) => {
|
|
4600
|
+
const proj = resolveProject(registry, req, reply);
|
|
4601
|
+
if (!proj) return;
|
|
4602
|
+
let typeFilter;
|
|
4603
|
+
if (req.query.type) {
|
|
4604
|
+
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
4605
|
+
const parsed = [];
|
|
4606
|
+
for (const c of candidates) {
|
|
4607
|
+
const r = import_types20.DivergenceTypeSchema.safeParse(c);
|
|
4608
|
+
if (!r.success) {
|
|
4609
|
+
return reply.code(400).send({
|
|
4610
|
+
error: `unknown divergence type "${c}"`,
|
|
4611
|
+
allowed: import_types20.DivergenceTypeSchema.options
|
|
4612
|
+
});
|
|
4613
|
+
}
|
|
4614
|
+
parsed.push(r.data);
|
|
4615
|
+
}
|
|
4616
|
+
typeFilter = new Set(parsed);
|
|
4617
|
+
}
|
|
4618
|
+
let minConfidence;
|
|
4619
|
+
if (req.query.minConfidence !== void 0) {
|
|
4620
|
+
const n = Number(req.query.minConfidence);
|
|
4621
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
4622
|
+
return reply.code(400).send({
|
|
4623
|
+
error: "minConfidence must be a number in [0, 1]"
|
|
4624
|
+
});
|
|
4625
|
+
}
|
|
4626
|
+
minConfidence = n;
|
|
4627
|
+
}
|
|
4628
|
+
return computeDivergences(proj.graph, {
|
|
4629
|
+
...typeFilter ? { type: typeFilter } : {},
|
|
4630
|
+
...minConfidence !== void 0 ? { minConfidence } : {},
|
|
4631
|
+
...req.query.node ? { node: req.query.node } : {}
|
|
4632
|
+
});
|
|
4633
|
+
});
|
|
4362
4634
|
scope.get("/incidents", async (req, reply) => {
|
|
4363
4635
|
const proj = resolveProject(registry, req, reply);
|
|
4364
4636
|
if (!proj) return;
|
|
@@ -4512,7 +4784,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4512
4784
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4513
4785
|
let violations = await log.readAll();
|
|
4514
4786
|
if (req.query.severity) {
|
|
4515
|
-
const sev =
|
|
4787
|
+
const sev = import_types20.PolicySeveritySchema.safeParse(req.query.severity);
|
|
4516
4788
|
if (!sev.success) {
|
|
4517
4789
|
return reply.code(400).send({
|
|
4518
4790
|
error: "invalid severity",
|
|
@@ -4529,7 +4801,7 @@ function registerRoutes(scope, ctx) {
|
|
|
4529
4801
|
scope.post("/policies/check", async (req, reply) => {
|
|
4530
4802
|
const proj = resolveProject(registry, req, reply);
|
|
4531
4803
|
if (!proj) return;
|
|
4532
|
-
const parsed =
|
|
4804
|
+
const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4533
4805
|
if (!parsed.success) {
|
|
4534
4806
|
return reply.code(400).send({
|
|
4535
4807
|
error: "invalid /policies/check body",
|
|
@@ -4615,13 +4887,13 @@ async function buildApi(opts) {
|
|
|
4615
4887
|
|
|
4616
4888
|
// src/extract/retire.ts
|
|
4617
4889
|
init_cjs_shims();
|
|
4618
|
-
var
|
|
4890
|
+
var import_types21 = require("@neat.is/types");
|
|
4619
4891
|
function retireEdgesByFile(graph, file) {
|
|
4620
4892
|
const normalized = file.split("\\").join("/");
|
|
4621
4893
|
const toDrop = [];
|
|
4622
4894
|
graph.forEachEdge((id, attrs) => {
|
|
4623
4895
|
const edge = attrs;
|
|
4624
|
-
if (edge.provenance !==
|
|
4896
|
+
if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
|
|
4625
4897
|
if (!edge.evidence?.file) return;
|
|
4626
4898
|
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
4627
4899
|
});
|
|
@@ -5613,9 +5885,12 @@ function renderPatch(sections) {
|
|
|
5613
5885
|
return lines.join("\n");
|
|
5614
5886
|
}
|
|
5615
5887
|
|
|
5888
|
+
// src/cli.ts
|
|
5889
|
+
var import_types23 = require("@neat.is/types");
|
|
5890
|
+
|
|
5616
5891
|
// src/cli-client.ts
|
|
5617
5892
|
init_cjs_shims();
|
|
5618
|
-
var
|
|
5893
|
+
var import_types22 = require("@neat.is/types");
|
|
5619
5894
|
var HttpError = class extends Error {
|
|
5620
5895
|
constructor(status2, message, responseBody = "") {
|
|
5621
5896
|
super(message);
|
|
@@ -5750,7 +6025,7 @@ async function runBlastRadius(client, input) {
|
|
|
5750
6025
|
}
|
|
5751
6026
|
}
|
|
5752
6027
|
function formatBlastEntry(n) {
|
|
5753
|
-
const tag = n.edgeProvenance ===
|
|
6028
|
+
const tag = n.edgeProvenance === import_types22.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
5754
6029
|
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
5755
6030
|
}
|
|
5756
6031
|
async function runDependencies(client, input) {
|
|
@@ -5796,9 +6071,9 @@ async function runObservedDependencies(client, input) {
|
|
|
5796
6071
|
const edges = await client.get(
|
|
5797
6072
|
projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
|
|
5798
6073
|
);
|
|
5799
|
-
const observed = edges.outbound.filter((e) => e.provenance ===
|
|
6074
|
+
const observed = edges.outbound.filter((e) => e.provenance === import_types22.Provenance.OBSERVED);
|
|
5800
6075
|
if (observed.length === 0) {
|
|
5801
|
-
const hasExtracted = edges.outbound.some((e) => e.provenance ===
|
|
6076
|
+
const hasExtracted = edges.outbound.some((e) => e.provenance === import_types22.Provenance.EXTRACTED);
|
|
5802
6077
|
const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
5803
6078
|
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
5804
6079
|
}
|
|
@@ -5806,7 +6081,7 @@ async function runObservedDependencies(client, input) {
|
|
|
5806
6081
|
return {
|
|
5807
6082
|
summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
5808
6083
|
block: blockLines.join("\n"),
|
|
5809
|
-
provenance:
|
|
6084
|
+
provenance: import_types22.Provenance.OBSERVED
|
|
5810
6085
|
};
|
|
5811
6086
|
} catch (err) {
|
|
5812
6087
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -5859,7 +6134,7 @@ async function runIncidents(client, input) {
|
|
|
5859
6134
|
return {
|
|
5860
6135
|
summary: `${target} has ${events.length} recorded incident${events.length === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
5861
6136
|
block: blockLines.join("\n"),
|
|
5862
|
-
provenance:
|
|
6137
|
+
provenance: import_types22.Provenance.OBSERVED
|
|
5863
6138
|
};
|
|
5864
6139
|
} catch (err) {
|
|
5865
6140
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -5967,7 +6242,7 @@ async function runStaleEdges(client, input) {
|
|
|
5967
6242
|
return {
|
|
5968
6243
|
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
5969
6244
|
block: blockLines.join("\n"),
|
|
5970
|
-
provenance:
|
|
6245
|
+
provenance: import_types22.Provenance.STALE
|
|
5971
6246
|
};
|
|
5972
6247
|
}
|
|
5973
6248
|
async function runPolicies(client, input) {
|
|
@@ -6030,6 +6305,54 @@ async function runPolicies(client, input) {
|
|
|
6030
6305
|
provenance: severities.join(" ")
|
|
6031
6306
|
};
|
|
6032
6307
|
}
|
|
6308
|
+
function formatDivergenceLine(d) {
|
|
6309
|
+
switch (d.type) {
|
|
6310
|
+
case "missing-observed":
|
|
6311
|
+
case "missing-extracted":
|
|
6312
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} (${d.edgeType}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
6313
|
+
case "version-mismatch":
|
|
6314
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared ${d.extractedVersion}, observed engine ${d.observedVersion} (${d.compatibility})`;
|
|
6315
|
+
case "host-mismatch":
|
|
6316
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared host ${d.extractedHost}, observed host ${d.observedHost}`;
|
|
6317
|
+
case "compat-violation":
|
|
6318
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
|
|
6319
|
+
}
|
|
6320
|
+
}
|
|
6321
|
+
async function runDivergences(client, input) {
|
|
6322
|
+
const params = new URLSearchParams();
|
|
6323
|
+
if (input.type && input.type.length > 0) params.set("type", input.type.join(","));
|
|
6324
|
+
if (input.minConfidence !== void 0) {
|
|
6325
|
+
params.set("minConfidence", String(input.minConfidence));
|
|
6326
|
+
}
|
|
6327
|
+
if (input.node) params.set("node", input.node);
|
|
6328
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
6329
|
+
const result = await client.get(
|
|
6330
|
+
projectPath(input.project, `/graph/divergences${qs}`)
|
|
6331
|
+
);
|
|
6332
|
+
if (result.totalAffected === 0) {
|
|
6333
|
+
return {
|
|
6334
|
+
summary: "No divergences found between the declared (EXTRACTED) and observed (OBSERVED) views of the graph."
|
|
6335
|
+
};
|
|
6336
|
+
}
|
|
6337
|
+
const headline = result.divergences[0];
|
|
6338
|
+
const summary = `Found ${result.totalAffected} divergence${result.totalAffected === 1 ? "" : "s"} between code and production. Highest-confidence: ${headline.type} on ${headline.source} \u2192 ${headline.target}. ${headline.reason}`;
|
|
6339
|
+
const blockLines = [];
|
|
6340
|
+
for (const d of result.divergences) {
|
|
6341
|
+
blockLines.push(formatDivergenceLine(d));
|
|
6342
|
+
blockLines.push(` reason: ${d.reason}`);
|
|
6343
|
+
blockLines.push(` recommendation: ${d.recommendation}`);
|
|
6344
|
+
}
|
|
6345
|
+
const maxConfidence = result.divergences.reduce(
|
|
6346
|
+
(m, d) => Math.max(m, d.confidence),
|
|
6347
|
+
0
|
|
6348
|
+
);
|
|
6349
|
+
return {
|
|
6350
|
+
summary,
|
|
6351
|
+
block: blockLines.join("\n"),
|
|
6352
|
+
confidence: maxConfidence,
|
|
6353
|
+
provenance: "composite (EXTRACTED + OBSERVED)"
|
|
6354
|
+
};
|
|
6355
|
+
}
|
|
6033
6356
|
function formatFooter(confidence, provenance) {
|
|
6034
6357
|
const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
|
|
6035
6358
|
const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
|
|
@@ -6108,6 +6431,9 @@ function usage() {
|
|
|
6108
6431
|
console.log(" policies Current policy violations.");
|
|
6109
6432
|
console.log(" Flags: --node <id>, --hypothetical-action <json>");
|
|
6110
6433
|
console.log(" example: neat policies --node service:<name> --json");
|
|
6434
|
+
console.log(" divergences Where code (EXTRACTED) and production (OBSERVED) disagree.");
|
|
6435
|
+
console.log(" Flags: --type <list>, --min-confidence <0..1>, --node <id>");
|
|
6436
|
+
console.log(" example: neat divergences --min-confidence 0.7");
|
|
6111
6437
|
console.log("");
|
|
6112
6438
|
console.log("flags:");
|
|
6113
6439
|
console.log(' --project <name> Name the project this command targets. Default: "default".');
|
|
@@ -6132,7 +6458,9 @@ var STRING_FLAGS = [
|
|
|
6132
6458
|
["--since", "since"],
|
|
6133
6459
|
["--against", "against"],
|
|
6134
6460
|
["--error-id", "errorId"],
|
|
6135
|
-
["--hypothetical-action", "hypotheticalAction"]
|
|
6461
|
+
["--hypothetical-action", "hypotheticalAction"],
|
|
6462
|
+
["--type", "type"],
|
|
6463
|
+
["--min-confidence", "minConfidence"]
|
|
6136
6464
|
];
|
|
6137
6465
|
function parseArgs(rest) {
|
|
6138
6466
|
const positional = [];
|
|
@@ -6151,6 +6479,8 @@ function parseArgs(rest) {
|
|
|
6151
6479
|
against: null,
|
|
6152
6480
|
errorId: null,
|
|
6153
6481
|
hypotheticalAction: null,
|
|
6482
|
+
type: null,
|
|
6483
|
+
minConfidence: null,
|
|
6154
6484
|
positional: []
|
|
6155
6485
|
};
|
|
6156
6486
|
for (let i = 0; i < rest.length; i++) {
|
|
@@ -6210,6 +6540,15 @@ function assignFlag(out, field, value) {
|
|
|
6210
6540
|
out[field] = n;
|
|
6211
6541
|
return;
|
|
6212
6542
|
}
|
|
6543
|
+
if (field === "minConfidence") {
|
|
6544
|
+
const n = Number(value);
|
|
6545
|
+
if (!Number.isFinite(n) || n < 0 || n > 1) {
|
|
6546
|
+
console.error("neat: --min-confidence must be a number in [0, 1]");
|
|
6547
|
+
process.exit(2);
|
|
6548
|
+
}
|
|
6549
|
+
out.minConfidence = n;
|
|
6550
|
+
return;
|
|
6551
|
+
}
|
|
6213
6552
|
;
|
|
6214
6553
|
out[field] = value;
|
|
6215
6554
|
}
|
|
@@ -6593,7 +6932,9 @@ var QUERY_VERBS = /* @__PURE__ */ new Set([
|
|
|
6593
6932
|
"search",
|
|
6594
6933
|
"diff",
|
|
6595
6934
|
"stale-edges",
|
|
6596
|
-
"policies"
|
|
6935
|
+
"policies",
|
|
6936
|
+
// Tenth verb (ADR-060) — amends ADR-050's locked allowlist of nine.
|
|
6937
|
+
"divergences"
|
|
6597
6938
|
]);
|
|
6598
6939
|
function resolveProjectFlag(parsed) {
|
|
6599
6940
|
if (parsed.project) return parsed.project;
|
|
@@ -6715,6 +7056,31 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
6715
7056
|
});
|
|
6716
7057
|
break;
|
|
6717
7058
|
}
|
|
7059
|
+
case "divergences": {
|
|
7060
|
+
let typeFilter;
|
|
7061
|
+
if (parsed.type) {
|
|
7062
|
+
const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
7063
|
+
const out = [];
|
|
7064
|
+
for (const p of parts) {
|
|
7065
|
+
const r = import_types23.DivergenceTypeSchema.safeParse(p);
|
|
7066
|
+
if (!r.success) {
|
|
7067
|
+
console.error(
|
|
7068
|
+
`neat divergences: unknown --type "${p}". allowed: ${import_types23.DivergenceTypeSchema.options.join(", ")}`
|
|
7069
|
+
);
|
|
7070
|
+
return 2;
|
|
7071
|
+
}
|
|
7072
|
+
out.push(r.data);
|
|
7073
|
+
}
|
|
7074
|
+
typeFilter = out;
|
|
7075
|
+
}
|
|
7076
|
+
work = runDivergences(client, {
|
|
7077
|
+
...typeFilter ? { type: typeFilter } : {},
|
|
7078
|
+
...parsed.minConfidence !== null ? { minConfidence: parsed.minConfidence } : {},
|
|
7079
|
+
...parsed.node ? { node: parsed.node } : {},
|
|
7080
|
+
...project ? { project } : {}
|
|
7081
|
+
});
|
|
7082
|
+
break;
|
|
7083
|
+
}
|
|
6718
7084
|
default:
|
|
6719
7085
|
console.error(`neat: unknown query verb "${cmd}"`);
|
|
6720
7086
|
return 2;
|