@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/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 import_types19 = require("@neat.is/types");
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 import_types18 = require("@neat.is/types");
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 import_types18.RegistryFileSchema.parse(parsed);
4363
+ return import_types19.RegistryFileSchema.parse(parsed);
4127
4364
  }
4128
4365
  async function writeRegistry(reg) {
4129
- const validated = import_types18.RegistryFileSchema.parse(reg);
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 {
@@ -4169,6 +4406,10 @@ async function addProject(opts) {
4169
4406
  return entry2;
4170
4407
  });
4171
4408
  }
4409
+ async function getProject(name) {
4410
+ const reg = await readRegistry();
4411
+ return reg.projects.find((p) => p.name === name);
4412
+ }
4172
4413
  async function listProjects() {
4173
4414
  const reg = await readRegistry();
4174
4415
  return reg.projects;
@@ -4305,9 +4546,15 @@ function registerRoutes(scope, ctx) {
4305
4546
  scope.get("/health", async (req, reply) => {
4306
4547
  const proj = resolveProject(registry, req, reply);
4307
4548
  if (!proj) return;
4549
+ const uptimeMs = Date.now() - startedAt;
4308
4550
  return {
4309
- uptime: Math.floor((Date.now() - startedAt) / 1e3),
4551
+ ok: true,
4310
4552
  project: proj.name,
4553
+ uptimeMs,
4554
+ // Legacy fields kept additively. The web shell's StatusBar reads
4555
+ // nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
4556
+ // the canonical triple and lets the extras pass through.
4557
+ uptime: Math.floor(uptimeMs / 1e3),
4311
4558
  nodeCount: proj.graph.order,
4312
4559
  edgeCount: proj.graph.size,
4313
4560
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
@@ -4327,7 +4574,7 @@ function registerRoutes(scope, ctx) {
4327
4574
  if (!proj.graph.hasNode(id)) {
4328
4575
  return reply.code(404).send({ error: "node not found", id });
4329
4576
  }
4330
- return proj.graph.getNodeAttributes(id);
4577
+ return { node: proj.graph.getNodeAttributes(id) };
4331
4578
  }
4332
4579
  );
4333
4580
  scope.get(
@@ -4344,12 +4591,12 @@ function registerRoutes(scope, ctx) {
4344
4591
  return { inbound, outbound };
4345
4592
  }
4346
4593
  );
4347
- scope.get("/graph/node/:id/dependencies", async (req, reply) => {
4594
+ scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
4348
4595
  const proj = resolveProject(registry, req, reply);
4349
4596
  if (!proj) return;
4350
- const { id } = req.params;
4351
- if (!proj.graph.hasNode(id)) {
4352
- return reply.code(404).send({ error: "node not found", id });
4597
+ const { nodeId } = req.params;
4598
+ if (!proj.graph.hasNode(nodeId)) {
4599
+ return reply.code(404).send({ error: "node not found", id: nodeId });
4353
4600
  }
4354
4601
  const depth = req.query.depth ? Number(req.query.depth) : TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH;
4355
4602
  if (!Number.isFinite(depth) || depth < 1 || depth > TRANSITIVE_DEPENDENCIES_MAX_DEPTH) {
@@ -4357,25 +4604,67 @@ function registerRoutes(scope, ctx) {
4357
4604
  error: `depth must be an integer in [1, ${TRANSITIVE_DEPENDENCIES_MAX_DEPTH}]`
4358
4605
  });
4359
4606
  }
4360
- return getTransitiveDependencies(proj.graph, id, depth);
4607
+ return getTransitiveDependencies(proj.graph, nodeId, depth);
4608
+ });
4609
+ scope.get("/graph/divergences", async (req, reply) => {
4610
+ const proj = resolveProject(registry, req, reply);
4611
+ if (!proj) return;
4612
+ let typeFilter;
4613
+ if (req.query.type) {
4614
+ const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4615
+ const parsed = [];
4616
+ for (const c of candidates) {
4617
+ const r = import_types20.DivergenceTypeSchema.safeParse(c);
4618
+ if (!r.success) {
4619
+ return reply.code(400).send({
4620
+ error: `unknown divergence type "${c}"`,
4621
+ allowed: import_types20.DivergenceTypeSchema.options
4622
+ });
4623
+ }
4624
+ parsed.push(r.data);
4625
+ }
4626
+ typeFilter = new Set(parsed);
4627
+ }
4628
+ let minConfidence;
4629
+ if (req.query.minConfidence !== void 0) {
4630
+ const n = Number(req.query.minConfidence);
4631
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
4632
+ return reply.code(400).send({
4633
+ error: "minConfidence must be a number in [0, 1]"
4634
+ });
4635
+ }
4636
+ minConfidence = n;
4637
+ }
4638
+ return computeDivergences(proj.graph, {
4639
+ ...typeFilter ? { type: typeFilter } : {},
4640
+ ...minConfidence !== void 0 ? { minConfidence } : {},
4641
+ ...req.query.node ? { node: req.query.node } : {}
4642
+ });
4361
4643
  });
4362
4644
  scope.get("/incidents", async (req, reply) => {
4363
4645
  const proj = resolveProject(registry, req, reply);
4364
4646
  if (!proj) return;
4365
4647
  const epath = errorsPathFor(proj);
4366
- if (!epath) return [];
4367
- return readErrorEvents(epath);
4648
+ if (!epath) return { count: 0, total: 0, events: [] };
4649
+ const events = await readErrorEvents(epath);
4650
+ const total = events.length;
4651
+ const limit = req.query.limit ? Number(req.query.limit) : 50;
4652
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
4653
+ const sliced = events.slice(0, safeLimit);
4654
+ return { count: sliced.length, total, events: sliced };
4368
4655
  });
4369
- scope.get("/incidents/stale", async (req, reply) => {
4656
+ scope.get("/stale-events", async (req, reply) => {
4370
4657
  const proj = resolveProject(registry, req, reply);
4371
4658
  if (!proj) return;
4372
4659
  const spath = staleEventsPathFor(proj);
4373
- if (!spath) return [];
4660
+ if (!spath) return { count: 0, total: 0, events: [] };
4374
4661
  const events = await readStaleEvents(spath);
4375
4662
  const filtered = req.query.edgeType ? events.filter((e) => e.edgeType === req.query.edgeType) : events;
4376
4663
  const ordered = [...filtered].reverse();
4664
+ const total = ordered.length;
4377
4665
  const limit = req.query.limit ? Number(req.query.limit) : 50;
4378
- return ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
4666
+ const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
4667
+ return { count: sliced.length, total, events: sliced };
4379
4668
  });
4380
4669
  scope.get(
4381
4670
  "/incidents/:nodeId",
@@ -4387,14 +4676,15 @@ function registerRoutes(scope, ctx) {
4387
4676
  return reply.code(404).send({ error: "node not found", id: nodeId });
4388
4677
  }
4389
4678
  const epath = errorsPathFor(proj);
4390
- if (!epath) return [];
4679
+ if (!epath) return { count: 0, total: 0, events: [] };
4391
4680
  const events = await readErrorEvents(epath);
4392
- return events.filter(
4681
+ const filtered = events.filter(
4393
4682
  (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
4394
4683
  );
4684
+ return { count: filtered.length, total: filtered.length, events: filtered };
4395
4685
  }
4396
4686
  );
4397
- scope.get("/traverse/root-cause/:nodeId", async (req, reply) => {
4687
+ scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
4398
4688
  const proj = resolveProject(registry, req, reply);
4399
4689
  if (!proj) return;
4400
4690
  const { nodeId } = req.params;
@@ -4414,7 +4704,7 @@ function registerRoutes(scope, ctx) {
4414
4704
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
4415
4705
  return result;
4416
4706
  });
4417
- scope.get("/traverse/blast-radius/:nodeId", async (req, reply) => {
4707
+ scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
4418
4708
  const proj = resolveProject(registry, req, reply);
4419
4709
  if (!proj) return;
4420
4710
  const { nodeId } = req.params;
@@ -4512,7 +4802,7 @@ function registerRoutes(scope, ctx) {
4512
4802
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4513
4803
  let violations = await log.readAll();
4514
4804
  if (req.query.severity) {
4515
- const sev = import_types19.PolicySeveritySchema.safeParse(req.query.severity);
4805
+ const sev = import_types20.PolicySeveritySchema.safeParse(req.query.severity);
4516
4806
  if (!sev.success) {
4517
4807
  return reply.code(400).send({
4518
4808
  error: "invalid severity",
@@ -4524,12 +4814,12 @@ function registerRoutes(scope, ctx) {
4524
4814
  if (req.query.policyId) {
4525
4815
  violations = violations.filter((v) => v.policyId === req.query.policyId);
4526
4816
  }
4527
- return violations;
4817
+ return { violations };
4528
4818
  });
4529
4819
  scope.post("/policies/check", async (req, reply) => {
4530
4820
  const proj = resolveProject(registry, req, reply);
4531
4821
  if (!proj) return;
4532
- const parsed = import_types19.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4822
+ const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4533
4823
  if (!parsed.success) {
4534
4824
  return reply.code(400).send({
4535
4825
  error: "invalid /policies/check body",
@@ -4603,6 +4893,20 @@ async function buildApi(opts) {
4603
4893
  });
4604
4894
  }
4605
4895
  });
4896
+ app.get("/projects/:project", async (req, reply) => {
4897
+ try {
4898
+ const entry2 = await getProject(req.params.project);
4899
+ if (!entry2) {
4900
+ return reply.code(404).send({ error: "project not found", project: req.params.project });
4901
+ }
4902
+ return { project: entry2 };
4903
+ } catch (err) {
4904
+ return reply.code(500).send({
4905
+ error: "failed to read project registry",
4906
+ details: err.message
4907
+ });
4908
+ }
4909
+ });
4606
4910
  registerRoutes(app, routeCtx);
4607
4911
  await app.register(
4608
4912
  async (scope) => {
@@ -4615,13 +4919,13 @@ async function buildApi(opts) {
4615
4919
 
4616
4920
  // src/extract/retire.ts
4617
4921
  init_cjs_shims();
4618
- var import_types20 = require("@neat.is/types");
4922
+ var import_types21 = require("@neat.is/types");
4619
4923
  function retireEdgesByFile(graph, file) {
4620
4924
  const normalized = file.split("\\").join("/");
4621
4925
  const toDrop = [];
4622
4926
  graph.forEachEdge((id, attrs) => {
4623
4927
  const edge = attrs;
4624
- if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
4928
+ if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
4625
4929
  if (!edge.evidence?.file) return;
4626
4930
  if (edge.evidence.file === normalized) toDrop.push(id);
4627
4931
  });
@@ -5613,9 +5917,12 @@ function renderPatch(sections) {
5613
5917
  return lines.join("\n");
5614
5918
  }
5615
5919
 
5920
+ // src/cli.ts
5921
+ var import_types23 = require("@neat.is/types");
5922
+
5616
5923
  // src/cli-client.ts
5617
5924
  init_cjs_shims();
5618
- var import_types21 = require("@neat.is/types");
5925
+ var import_types22 = require("@neat.is/types");
5619
5926
  var HttpError = class extends Error {
5620
5927
  constructor(status2, message, responseBody = "") {
5621
5928
  super(message);
@@ -5687,7 +5994,7 @@ async function runRootCause(client, input) {
5687
5994
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
5688
5995
  const path38 = projectPath(
5689
5996
  input.project,
5690
- `/traverse/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
5997
+ `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
5691
5998
  );
5692
5999
  try {
5693
6000
  const result = await client.get(path38);
@@ -5718,7 +6025,7 @@ async function runBlastRadius(client, input) {
5718
6025
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
5719
6026
  const path38 = projectPath(
5720
6027
  input.project,
5721
- `/traverse/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
6028
+ `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
5722
6029
  );
5723
6030
  try {
5724
6031
  const result = await client.get(path38);
@@ -5750,14 +6057,14 @@ async function runBlastRadius(client, input) {
5750
6057
  }
5751
6058
  }
5752
6059
  function formatBlastEntry(n) {
5753
- const tag = n.edgeProvenance === import_types21.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
6060
+ const tag = n.edgeProvenance === import_types22.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
5754
6061
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
5755
6062
  }
5756
6063
  async function runDependencies(client, input) {
5757
6064
  const depth = input.depth ?? 3;
5758
6065
  const path38 = projectPath(
5759
6066
  input.project,
5760
- `/graph/node/${encodeURIComponent(input.nodeId)}/dependencies?depth=${depth}`
6067
+ `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
5761
6068
  );
5762
6069
  try {
5763
6070
  const result = await client.get(path38);
@@ -5796,9 +6103,9 @@ async function runObservedDependencies(client, input) {
5796
6103
  const edges = await client.get(
5797
6104
  projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
5798
6105
  );
5799
- const observed = edges.outbound.filter((e) => e.provenance === import_types21.Provenance.OBSERVED);
6106
+ const observed = edges.outbound.filter((e) => e.provenance === import_types22.Provenance.OBSERVED);
5800
6107
  if (observed.length === 0) {
5801
- const hasExtracted = edges.outbound.some((e) => e.provenance === import_types21.Provenance.EXTRACTED);
6108
+ const hasExtracted = edges.outbound.some((e) => e.provenance === import_types22.Provenance.EXTRACTED);
5802
6109
  const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
5803
6110
  return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
5804
6111
  }
@@ -5806,7 +6113,7 @@ async function runObservedDependencies(client, input) {
5806
6113
  return {
5807
6114
  summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
5808
6115
  block: blockLines.join("\n"),
5809
- provenance: import_types21.Provenance.OBSERVED
6116
+ provenance: import_types22.Provenance.OBSERVED
5810
6117
  };
5811
6118
  } catch (err) {
5812
6119
  if (err instanceof HttpError && err.status === 404) {
@@ -5843,7 +6150,8 @@ function formatDuration(ms) {
5843
6150
  async function runIncidents(client, input) {
5844
6151
  const path38 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
5845
6152
  try {
5846
- const events = await client.get(path38);
6153
+ const body = await client.get(path38);
6154
+ const events = body.events;
5847
6155
  if (events.length === 0) {
5848
6156
  return {
5849
6157
  summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
@@ -5857,9 +6165,9 @@ async function runIncidents(client, input) {
5857
6165
  }
5858
6166
  const target = input.nodeId ?? "the project";
5859
6167
  return {
5860
- summary: `${target} has ${events.length} recorded incident${events.length === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
6168
+ summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
5861
6169
  block: blockLines.join("\n"),
5862
- provenance: import_types21.Provenance.OBSERVED
6170
+ provenance: import_types22.Provenance.OBSERVED
5863
6171
  };
5864
6172
  } catch (err) {
5865
6173
  if (err instanceof HttpError && err.status === 404) {
@@ -5953,9 +6261,10 @@ async function runStaleEdges(client, input) {
5953
6261
  if (input.limit !== void 0) params.set("limit", String(input.limit));
5954
6262
  if (input.edgeType) params.set("edgeType", input.edgeType);
5955
6263
  const qs = params.size > 0 ? `?${params.toString()}` : "";
5956
- const events = await client.get(
5957
- projectPath(input.project, `/incidents/stale${qs}`)
6264
+ const body = await client.get(
6265
+ projectPath(input.project, `/stale-events${qs}`)
5958
6266
  );
6267
+ const events = body.events;
5959
6268
  if (events.length === 0) {
5960
6269
  return {
5961
6270
  summary: input.edgeType ? `No stale ${input.edgeType} edges recorded.` : "No stale-edge transitions recorded yet."
@@ -5967,7 +6276,7 @@ async function runStaleEdges(client, input) {
5967
6276
  return {
5968
6277
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
5969
6278
  block: blockLines.join("\n"),
5970
- provenance: import_types21.Provenance.STALE
6279
+ provenance: import_types22.Provenance.STALE
5971
6280
  };
5972
6281
  }
5973
6282
  async function runPolicies(client, input) {
@@ -5989,9 +6298,10 @@ async function runPolicies(client, input) {
5989
6298
  const params = new URLSearchParams();
5990
6299
  if (input.policyId) params.set("policyId", input.policyId);
5991
6300
  const qs = params.size > 0 ? `?${params.toString()}` : "";
5992
- violations = await client.get(
6301
+ const body = await client.get(
5993
6302
  projectPath(input.project, `/policies/violations${qs}`)
5994
6303
  );
6304
+ violations = body.violations;
5995
6305
  allowed = violations.every((v) => v.onViolation !== "block");
5996
6306
  }
5997
6307
  if (input.nodeId) {
@@ -6030,6 +6340,54 @@ async function runPolicies(client, input) {
6030
6340
  provenance: severities.join(" ")
6031
6341
  };
6032
6342
  }
6343
+ function formatDivergenceLine(d) {
6344
+ switch (d.type) {
6345
+ case "missing-observed":
6346
+ case "missing-extracted":
6347
+ return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} (${d.edgeType}) \u2014 confidence ${d.confidence.toFixed(2)}`;
6348
+ case "version-mismatch":
6349
+ return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared ${d.extractedVersion}, observed engine ${d.observedVersion} (${d.compatibility})`;
6350
+ case "host-mismatch":
6351
+ return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared host ${d.extractedHost}, observed host ${d.observedHost}`;
6352
+ case "compat-violation":
6353
+ return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
6354
+ }
6355
+ }
6356
+ async function runDivergences(client, input) {
6357
+ const params = new URLSearchParams();
6358
+ if (input.type && input.type.length > 0) params.set("type", input.type.join(","));
6359
+ if (input.minConfidence !== void 0) {
6360
+ params.set("minConfidence", String(input.minConfidence));
6361
+ }
6362
+ if (input.node) params.set("node", input.node);
6363
+ const qs = params.size > 0 ? `?${params.toString()}` : "";
6364
+ const result = await client.get(
6365
+ projectPath(input.project, `/graph/divergences${qs}`)
6366
+ );
6367
+ if (result.totalAffected === 0) {
6368
+ return {
6369
+ summary: "No divergences found between the declared (EXTRACTED) and observed (OBSERVED) views of the graph."
6370
+ };
6371
+ }
6372
+ const headline = result.divergences[0];
6373
+ 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}`;
6374
+ const blockLines = [];
6375
+ for (const d of result.divergences) {
6376
+ blockLines.push(formatDivergenceLine(d));
6377
+ blockLines.push(` reason: ${d.reason}`);
6378
+ blockLines.push(` recommendation: ${d.recommendation}`);
6379
+ }
6380
+ const maxConfidence = result.divergences.reduce(
6381
+ (m, d) => Math.max(m, d.confidence),
6382
+ 0
6383
+ );
6384
+ return {
6385
+ summary,
6386
+ block: blockLines.join("\n"),
6387
+ confidence: maxConfidence,
6388
+ provenance: "composite (EXTRACTED + OBSERVED)"
6389
+ };
6390
+ }
6033
6391
  function formatFooter(confidence, provenance) {
6034
6392
  const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
6035
6393
  const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
@@ -6108,6 +6466,9 @@ function usage() {
6108
6466
  console.log(" policies Current policy violations.");
6109
6467
  console.log(" Flags: --node <id>, --hypothetical-action <json>");
6110
6468
  console.log(" example: neat policies --node service:<name> --json");
6469
+ console.log(" divergences Where code (EXTRACTED) and production (OBSERVED) disagree.");
6470
+ console.log(" Flags: --type <list>, --min-confidence <0..1>, --node <id>");
6471
+ console.log(" example: neat divergences --min-confidence 0.7");
6111
6472
  console.log("");
6112
6473
  console.log("flags:");
6113
6474
  console.log(' --project <name> Name the project this command targets. Default: "default".');
@@ -6132,7 +6493,9 @@ var STRING_FLAGS = [
6132
6493
  ["--since", "since"],
6133
6494
  ["--against", "against"],
6134
6495
  ["--error-id", "errorId"],
6135
- ["--hypothetical-action", "hypotheticalAction"]
6496
+ ["--hypothetical-action", "hypotheticalAction"],
6497
+ ["--type", "type"],
6498
+ ["--min-confidence", "minConfidence"]
6136
6499
  ];
6137
6500
  function parseArgs(rest) {
6138
6501
  const positional = [];
@@ -6151,6 +6514,8 @@ function parseArgs(rest) {
6151
6514
  against: null,
6152
6515
  errorId: null,
6153
6516
  hypotheticalAction: null,
6517
+ type: null,
6518
+ minConfidence: null,
6154
6519
  positional: []
6155
6520
  };
6156
6521
  for (let i = 0; i < rest.length; i++) {
@@ -6210,6 +6575,15 @@ function assignFlag(out, field, value) {
6210
6575
  out[field] = n;
6211
6576
  return;
6212
6577
  }
6578
+ if (field === "minConfidence") {
6579
+ const n = Number(value);
6580
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
6581
+ console.error("neat: --min-confidence must be a number in [0, 1]");
6582
+ process.exit(2);
6583
+ }
6584
+ out.minConfidence = n;
6585
+ return;
6586
+ }
6213
6587
  ;
6214
6588
  out[field] = value;
6215
6589
  }
@@ -6593,7 +6967,9 @@ var QUERY_VERBS = /* @__PURE__ */ new Set([
6593
6967
  "search",
6594
6968
  "diff",
6595
6969
  "stale-edges",
6596
- "policies"
6970
+ "policies",
6971
+ // Tenth verb (ADR-060) — amends ADR-050's locked allowlist of nine.
6972
+ "divergences"
6597
6973
  ]);
6598
6974
  function resolveProjectFlag(parsed) {
6599
6975
  if (parsed.project) return parsed.project;
@@ -6715,6 +7091,31 @@ async function runQueryVerb(cmd, parsed) {
6715
7091
  });
6716
7092
  break;
6717
7093
  }
7094
+ case "divergences": {
7095
+ let typeFilter;
7096
+ if (parsed.type) {
7097
+ const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
7098
+ const out = [];
7099
+ for (const p of parts) {
7100
+ const r = import_types23.DivergenceTypeSchema.safeParse(p);
7101
+ if (!r.success) {
7102
+ console.error(
7103
+ `neat divergences: unknown --type "${p}". allowed: ${import_types23.DivergenceTypeSchema.options.join(", ")}`
7104
+ );
7105
+ return 2;
7106
+ }
7107
+ out.push(r.data);
7108
+ }
7109
+ typeFilter = out;
7110
+ }
7111
+ work = runDivergences(client, {
7112
+ ...typeFilter ? { type: typeFilter } : {},
7113
+ ...parsed.minConfidence !== null ? { minConfidence: parsed.minConfidence } : {},
7114
+ ...parsed.node ? { node: parsed.node } : {},
7115
+ ...project ? { project } : {}
7116
+ });
7117
+ break;
7118
+ }
6718
7119
  default:
6719
7120
  console.error(`neat: unknown query verb "${cmd}"`);
6720
7121
  return 2;