@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/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 {
@@ -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 = import_types19.PolicySeveritySchema.safeParse(req.query.severity);
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 = import_types19.PoliciesCheckBodySchema.safeParse(req.body ?? {});
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 import_types20 = require("@neat.is/types");
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 !== import_types20.Provenance.EXTRACTED) return;
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 import_types21 = require("@neat.is/types");
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 === import_types21.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
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 === import_types21.Provenance.OBSERVED);
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 === import_types21.Provenance.EXTRACTED);
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: import_types21.Provenance.OBSERVED
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: import_types21.Provenance.OBSERVED
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: import_types21.Provenance.STALE
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;