@neat.is/core 0.3.4 → 0.3.6

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
@@ -805,11 +805,16 @@ init_cjs_shims();
805
805
  var import_types = require("@neat.is/types");
806
806
  var ROOT_CAUSE_MAX_DEPTH = 5;
807
807
  var BLAST_RADIUS_DEFAULT_DEPTH = 10;
808
+ function isFrontierNode(graph, nodeId) {
809
+ if (!graph.hasNode(nodeId)) return false;
810
+ const attrs = graph.getNodeAttributes(nodeId);
811
+ return attrs.type === import_types.NodeType.FrontierNode;
812
+ }
808
813
  function bestEdgeBySource(graph, edgeIds) {
809
814
  const best = /* @__PURE__ */ new Map();
810
815
  for (const id of edgeIds) {
811
816
  const e = graph.getEdgeAttributes(id);
812
- if (e.provenance === import_types.Provenance.FRONTIER) continue;
817
+ if (isFrontierNode(graph, e.source)) continue;
813
818
  const cur = best.get(e.source);
814
819
  if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
815
820
  best.set(e.source, e);
@@ -821,7 +826,7 @@ function bestEdgeByTarget(graph, edgeIds) {
821
826
  const best = /* @__PURE__ */ new Map();
822
827
  for (const id of edgeIds) {
823
828
  const e = graph.getEdgeAttributes(id);
824
- if (e.provenance === import_types.Provenance.FRONTIER) continue;
829
+ if (isFrontierNode(graph, e.target)) continue;
825
830
  const cur = best.get(e.target);
826
831
  if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
827
832
  best.set(e.target, e);
@@ -833,8 +838,7 @@ var PROVENANCE_CEILING = {
833
838
  OBSERVED: 1,
834
839
  INFERRED: 0.7,
835
840
  EXTRACTED: 0.5,
836
- STALE: 0.3,
837
- FRONTIER: 0.3
841
+ STALE: 0.3
838
842
  };
839
843
  function volumeWeight(spanCount) {
840
844
  if (!spanCount || spanCount <= 0) return 0.5;
@@ -1118,8 +1122,8 @@ var evaluateStructural = ({
1118
1122
  for (const edgeId of graph.outboundEdges(id)) {
1119
1123
  const e = graph.getEdgeAttributes(edgeId);
1120
1124
  if (e.type !== rule.edgeType) continue;
1121
- if (e.provenance === import_types2.Provenance.FRONTIER) continue;
1122
1125
  const target = graph.getNodeAttributes(e.target);
1126
+ if (target.type === import_types2.NodeType.FrontierNode) continue;
1123
1127
  if (target.type === rule.toNodeType) {
1124
1128
  satisfied = true;
1125
1129
  break;
@@ -1238,8 +1242,8 @@ var evaluateCompatibility = ({
1238
1242
  for (const edgeId of graph.outboundEdges(svcId)) {
1239
1243
  const e = graph.getEdgeAttributes(edgeId);
1240
1244
  if (e.type !== import_types2.EdgeType.CONNECTS_TO) continue;
1241
- if (e.provenance === import_types2.Provenance.FRONTIER) continue;
1242
1245
  const dbAttrs = graph.getNodeAttributes(e.target);
1246
+ if (dbAttrs.type === import_types2.NodeType.FrontierNode) continue;
1243
1247
  if (dbAttrs.type !== import_types2.NodeType.DatabaseNode) continue;
1244
1248
  const db = dbAttrs;
1245
1249
  for (const pair of compatPairs()) {
@@ -1556,31 +1560,6 @@ function ensureFrontierNode(graph, host, ts) {
1556
1560
  graph.addNode(id, node);
1557
1561
  return id;
1558
1562
  }
1559
- function upsertFrontierEdge(graph, type, source, target, ts) {
1560
- const id = (0, import_types3.frontierEdgeId)(source, target, type);
1561
- if (graph.hasEdge(id)) {
1562
- const existing = graph.getEdgeAttributes(id);
1563
- const updated = {
1564
- ...existing,
1565
- provenance: import_types3.Provenance.FRONTIER,
1566
- lastObserved: ts,
1567
- callCount: (existing.callCount ?? 0) + 1
1568
- };
1569
- graph.replaceEdgeAttributes(id, updated);
1570
- return;
1571
- }
1572
- const edge = {
1573
- id,
1574
- source,
1575
- target,
1576
- type,
1577
- provenance: import_types3.Provenance.FRONTIER,
1578
- confidence: 1,
1579
- lastObserved: ts,
1580
- callCount: 1
1581
- };
1582
- graph.addEdgeWithKey(id, source, target, edge);
1583
- }
1584
1563
  function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1585
1564
  if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
1586
1565
  const id = makeObservedEdgeId(type, source, target);
@@ -1666,18 +1645,28 @@ async function appendErrorEvent(ctx, ev) {
1666
1645
  await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(ctx.errorsPath), { recursive: true });
1667
1646
  await import_node_fs3.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
1668
1647
  }
1648
+ function sanitizeAttributes(attrs) {
1649
+ const out = {};
1650
+ for (const [k, v] of Object.entries(attrs)) {
1651
+ if (typeof v === "bigint") out[k] = v.toString();
1652
+ else out[k] = v;
1653
+ }
1654
+ return out;
1655
+ }
1669
1656
  function buildErrorEventForReceiver(span) {
1670
1657
  if (span.statusCode !== 2) return null;
1671
1658
  const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
1659
+ const attrs = sanitizeAttributes(span.attributes);
1672
1660
  return {
1673
1661
  id: `${span.traceId}:${span.spanId}`,
1674
1662
  timestamp: ts,
1675
1663
  service: span.service,
1676
1664
  traceId: span.traceId,
1677
1665
  spanId: span.spanId,
1678
- errorMessage: span.exception?.message ?? span.errorMessage ?? span.name ?? "unknown error",
1666
+ errorMessage: span.exception?.message ?? "unknown error",
1679
1667
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1680
1668
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1669
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
1681
1670
  affectedNode: (0, import_types3.serviceId)(span.service)
1682
1671
  };
1683
1672
  }
@@ -1728,11 +1717,16 @@ async function handleSpan(ctx, span) {
1728
1717
  affectedNode = targetId;
1729
1718
  resolvedViaAddress = true;
1730
1719
  } else if (!targetId) {
1731
- const frontierId2 = ensureFrontierNode(ctx.graph, host, ts);
1732
- if (ctx.graph.hasNode(sourceId)) {
1733
- upsertFrontierEdge(ctx.graph, import_types3.EdgeType.CALLS, sourceId, frontierId2, ts);
1734
- }
1735
- affectedNode = frontierId2;
1720
+ const frontierNodeId = ensureFrontierNode(ctx.graph, host, ts);
1721
+ upsertObservedEdge(
1722
+ ctx.graph,
1723
+ import_types3.EdgeType.CALLS,
1724
+ sourceId,
1725
+ frontierNodeId,
1726
+ ts,
1727
+ isError
1728
+ );
1729
+ affectedNode = frontierNodeId;
1736
1730
  resolvedViaAddress = true;
1737
1731
  }
1738
1732
  }
@@ -1754,15 +1748,17 @@ async function handleSpan(ctx, span) {
1754
1748
  if (span.statusCode === 2) {
1755
1749
  stitchTrace(ctx.graph, sourceId, ts);
1756
1750
  if (ctx.writeErrorEventInline !== false) {
1751
+ const attrs = sanitizeAttributes(span.attributes);
1757
1752
  const ev = {
1758
1753
  id: `${span.traceId}:${span.spanId}`,
1759
1754
  timestamp: ts,
1760
1755
  service: span.service,
1761
1756
  traceId: span.traceId,
1762
1757
  spanId: span.spanId,
1763
- errorMessage: span.exception?.message ?? span.errorMessage ?? span.name ?? "unknown error",
1758
+ errorMessage: span.exception?.message ?? "unknown error",
1764
1759
  ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1765
1760
  ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1761
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
1766
1762
  affectedNode
1767
1763
  };
1768
1764
  await appendErrorEvent(ctx, ev);
@@ -1818,8 +1814,7 @@ function rewireFrontierEdges(graph, frontierId2, serviceId3) {
1818
1814
  }
1819
1815
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
1820
1816
  graph.dropEdge(oldEdgeId);
1821
- const promotedProvenance = edge.provenance === import_types3.Provenance.FRONTIER ? import_types3.Provenance.OBSERVED : edge.provenance;
1822
- const newId = promotedProvenance === import_types3.Provenance.OBSERVED ? (0, import_types3.observedEdgeId)(newSource, newTarget, edge.type) : promotedProvenance === import_types3.Provenance.INFERRED ? (0, import_types3.inferredEdgeId)(newSource, newTarget, edge.type) : promotedProvenance === import_types3.Provenance.EXTRACTED ? (0, import_types3.extractedEdgeId)(newSource, newTarget, edge.type) : (0, import_types3.frontierEdgeId)(newSource, newTarget, edge.type);
1817
+ const newId = edge.provenance === import_types3.Provenance.OBSERVED ? (0, import_types3.observedEdgeId)(newSource, newTarget, edge.type) : edge.provenance === import_types3.Provenance.INFERRED ? (0, import_types3.inferredEdgeId)(newSource, newTarget, edge.type) : (0, import_types3.extractedEdgeId)(newSource, newTarget, edge.type);
1823
1818
  if (graph.hasEdge(newId)) {
1824
1819
  const existing = graph.getEdgeAttributes(newId);
1825
1820
  const merged = {
@@ -1834,8 +1829,7 @@ function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
1834
1829
  ...edge,
1835
1830
  id: newId,
1836
1831
  source: newSource,
1837
- target: newTarget,
1838
- provenance: promotedProvenance
1832
+ target: newTarget
1839
1833
  };
1840
1834
  graph.addEdgeWithKey(newId, newSource, newTarget, rebuilt);
1841
1835
  }
@@ -4156,7 +4150,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4156
4150
  init_cjs_shims();
4157
4151
  var import_node_fs18 = require("fs");
4158
4152
  var import_node_path31 = __toESM(require("path"), 1);
4159
- var SCHEMA_VERSION = 2;
4153
+ var import_types19 = require("@neat.is/types");
4154
+ var SCHEMA_VERSION = 3;
4160
4155
  function migrateV1ToV2(payload) {
4161
4156
  const nodes = payload.graph.nodes;
4162
4157
  if (Array.isArray(nodes)) {
@@ -4168,6 +4163,25 @@ function migrateV1ToV2(payload) {
4168
4163
  }
4169
4164
  return { ...payload, schemaVersion: 2 };
4170
4165
  }
4166
+ function migrateV2ToV3(payload) {
4167
+ const edges = payload.graph.edges;
4168
+ if (Array.isArray(edges)) {
4169
+ for (const edge of edges) {
4170
+ const attrs = edge.attributes;
4171
+ if (!attrs || attrs.provenance !== "FRONTIER") continue;
4172
+ attrs.provenance = import_types19.Provenance.OBSERVED;
4173
+ const type = typeof attrs.type === "string" ? attrs.type : void 0;
4174
+ const source = typeof attrs.source === "string" ? attrs.source : void 0;
4175
+ const target = typeof attrs.target === "string" ? attrs.target : void 0;
4176
+ if (type && source && target) {
4177
+ const newId = (0, import_types19.observedEdgeId)(source, target, type);
4178
+ attrs.id = newId;
4179
+ if (edge.key) edge.key = newId;
4180
+ }
4181
+ }
4182
+ }
4183
+ return { ...payload, schemaVersion: 3 };
4184
+ }
4171
4185
  async function ensureDir(filePath) {
4172
4186
  await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
4173
4187
  }
@@ -4194,6 +4208,9 @@ async function loadGraphFromDisk(graph, outPath) {
4194
4208
  if (payload.schemaVersion === 1) {
4195
4209
  payload = migrateV1ToV2(payload);
4196
4210
  }
4211
+ if (payload.schemaVersion === 2) {
4212
+ payload = migrateV2ToV3(payload);
4213
+ }
4197
4214
  if (payload.schemaVersion !== SCHEMA_VERSION) {
4198
4215
  throw new Error(
4199
4216
  `persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
@@ -4246,11 +4263,11 @@ var import_chokidar = __toESM(require("chokidar"), 1);
4246
4263
  init_cjs_shims();
4247
4264
  var import_fastify = __toESM(require("fastify"), 1);
4248
4265
  var import_cors = __toESM(require("@fastify/cors"), 1);
4249
- var import_types21 = require("@neat.is/types");
4266
+ var import_types22 = require("@neat.is/types");
4250
4267
 
4251
4268
  // src/divergences.ts
4252
4269
  init_cjs_shims();
4253
- var import_types19 = require("@neat.is/types");
4270
+ var import_types20 = require("@neat.is/types");
4254
4271
  function bucketKey(source, target, type) {
4255
4272
  return `${type}|${source}|${target}`;
4256
4273
  }
@@ -4258,25 +4275,22 @@ function bucketEdges(graph) {
4258
4275
  const buckets = /* @__PURE__ */ new Map();
4259
4276
  graph.forEachEdge((id, attrs) => {
4260
4277
  const e = attrs;
4261
- const parsed = (0, import_types19.parseEdgeId)(id);
4278
+ const parsed = (0, import_types20.parseEdgeId)(id);
4262
4279
  const provenance = parsed?.provenance ?? e.provenance;
4263
4280
  const key = bucketKey(e.source, e.target, e.type);
4264
4281
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4265
4282
  switch (provenance) {
4266
- case import_types19.Provenance.EXTRACTED:
4283
+ case import_types20.Provenance.EXTRACTED:
4267
4284
  cur.extracted = e;
4268
4285
  break;
4269
- case import_types19.Provenance.OBSERVED:
4286
+ case import_types20.Provenance.OBSERVED:
4270
4287
  cur.observed = e;
4271
4288
  break;
4272
- case import_types19.Provenance.INFERRED:
4289
+ case import_types20.Provenance.INFERRED:
4273
4290
  cur.inferred = e;
4274
4291
  break;
4275
- case import_types19.Provenance.FRONTIER:
4276
- cur.frontier = e;
4277
- break;
4278
4292
  default:
4279
- if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
4293
+ if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
4280
4294
  }
4281
4295
  buckets.set(key, cur);
4282
4296
  });
@@ -4285,7 +4299,7 @@ function bucketEdges(graph) {
4285
4299
  function nodeIsFrontier(graph, nodeId) {
4286
4300
  if (!graph.hasNode(nodeId)) return false;
4287
4301
  const attrs = graph.getNodeAttributes(nodeId);
4288
- return attrs.type === import_types19.NodeType.FrontierNode;
4302
+ return attrs.type === import_types20.NodeType.FrontierNode;
4289
4303
  }
4290
4304
  function clampConfidence(n) {
4291
4305
  if (!Number.isFinite(n)) return 0;
@@ -4346,7 +4360,7 @@ function declaredHostFor(svc) {
4346
4360
  function hasExtractedConfiguredBy(graph, svcId) {
4347
4361
  for (const edgeId of graph.outboundEdges(svcId)) {
4348
4362
  const e = graph.getEdgeAttributes(edgeId);
4349
- if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
4363
+ if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
4350
4364
  return true;
4351
4365
  }
4352
4366
  }
@@ -4359,10 +4373,10 @@ function detectHostMismatch(graph, svcId, svc) {
4359
4373
  const out = [];
4360
4374
  for (const edgeId of graph.outboundEdges(svcId)) {
4361
4375
  const edge = graph.getEdgeAttributes(edgeId);
4362
- if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4363
- if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4376
+ if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4377
+ if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4364
4378
  const target = graph.getNodeAttributes(edge.target);
4365
- if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4379
+ if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4366
4380
  const observedHost = target.host?.trim();
4367
4381
  if (!observedHost) continue;
4368
4382
  if (observedHost === declaredHost) continue;
@@ -4384,10 +4398,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4384
4398
  const deps = svc.dependencies ?? {};
4385
4399
  for (const edgeId of graph.outboundEdges(svcId)) {
4386
4400
  const edge = graph.getEdgeAttributes(edgeId);
4387
- if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4388
- if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4401
+ if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4402
+ if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4389
4403
  const target = graph.getNodeAttributes(edge.target);
4390
- if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4404
+ if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4391
4405
  for (const pair of compatPairs()) {
4392
4406
  if (pair.engine !== target.engine) continue;
4393
4407
  const declared = deps[pair.driver];
@@ -4448,7 +4462,7 @@ function computeDivergences(graph, opts = {}) {
4448
4462
  }
4449
4463
  graph.forEachNode((nodeId, attrs) => {
4450
4464
  const n = attrs;
4451
- if (n.type !== import_types19.NodeType.ServiceNode) return;
4465
+ if (n.type !== import_types20.NodeType.ServiceNode) return;
4452
4466
  const svc = n;
4453
4467
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4454
4468
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4481,7 +4495,7 @@ function computeDivergences(graph, opts = {}) {
4481
4495
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4482
4496
  return a.target.localeCompare(b.target);
4483
4497
  });
4484
- return import_types19.DivergenceResultSchema.parse({
4498
+ return import_types20.DivergenceResultSchema.parse({
4485
4499
  divergences: filtered,
4486
4500
  totalAffected: filtered.length,
4487
4501
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4622,7 +4636,7 @@ init_cjs_shims();
4622
4636
  var import_node_fs20 = require("fs");
4623
4637
  var import_node_os2 = __toESM(require("os"), 1);
4624
4638
  var import_node_path33 = __toESM(require("path"), 1);
4625
- var import_types20 = require("@neat.is/types");
4639
+ var import_types21 = require("@neat.is/types");
4626
4640
  var LOCK_TIMEOUT_MS = 5e3;
4627
4641
  var LOCK_RETRY_MS = 50;
4628
4642
  function neatHome() {
@@ -4701,10 +4715,10 @@ async function readRegistry() {
4701
4715
  throw err;
4702
4716
  }
4703
4717
  const parsed = JSON.parse(raw);
4704
- return import_types20.RegistryFileSchema.parse(parsed);
4718
+ return import_types21.RegistryFileSchema.parse(parsed);
4705
4719
  }
4706
4720
  async function writeRegistry(reg) {
4707
- const validated = import_types20.RegistryFileSchema.parse(reg);
4721
+ const validated = import_types21.RegistryFileSchema.parse(reg);
4708
4722
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4709
4723
  }
4710
4724
  var ProjectNameCollisionError = class extends Error {
@@ -4955,11 +4969,11 @@ function registerRoutes(scope, ctx) {
4955
4969
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4956
4970
  const parsed = [];
4957
4971
  for (const c of candidates) {
4958
- const r = import_types21.DivergenceTypeSchema.safeParse(c);
4972
+ const r = import_types22.DivergenceTypeSchema.safeParse(c);
4959
4973
  if (!r.success) {
4960
4974
  return reply.code(400).send({
4961
4975
  error: `unknown divergence type "${c}"`,
4962
- allowed: import_types21.DivergenceTypeSchema.options
4976
+ allowed: import_types22.DivergenceTypeSchema.options
4963
4977
  });
4964
4978
  }
4965
4979
  parsed.push(r.data);
@@ -5143,7 +5157,7 @@ function registerRoutes(scope, ctx) {
5143
5157
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5144
5158
  let violations = await log.readAll();
5145
5159
  if (req.query.severity) {
5146
- const sev = import_types21.PolicySeveritySchema.safeParse(req.query.severity);
5160
+ const sev = import_types22.PolicySeveritySchema.safeParse(req.query.severity);
5147
5161
  if (!sev.success) {
5148
5162
  return reply.code(400).send({
5149
5163
  error: "invalid severity",
@@ -5160,7 +5174,7 @@ function registerRoutes(scope, ctx) {
5160
5174
  scope.post("/policies/check", async (req, reply) => {
5161
5175
  const proj = resolveProject(registry, req, reply);
5162
5176
  if (!proj) return;
5163
- const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5177
+ const parsed = import_types22.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5164
5178
  if (!parsed.success) {
5165
5179
  return reply.code(400).send({
5166
5180
  error: "invalid /policies/check body",
@@ -5897,15 +5911,69 @@ init_cjs_shims();
5897
5911
  init_cjs_shims();
5898
5912
  var import_node_fs23 = require("fs");
5899
5913
  var import_node_path38 = __toESM(require("path"), 1);
5914
+
5915
+ // src/installers/templates.ts
5916
+ init_cjs_shims();
5917
+ var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
5918
+ var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
5919
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
5920
+ // the auto-instrumentation hook attaches. Configure via the env file.
5921
+ const path = require('node:path')
5922
+ try {
5923
+ require('dotenv').config({ path: path.join(__dirname, '.env.neat') })
5924
+ } catch (err) {
5925
+ // dotenv unavailable \u2014 fall through to process.env.
5926
+ }
5927
+ require('@opentelemetry/auto-instrumentations-node/register')
5928
+ `;
5929
+ var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
5930
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
5931
+ // the auto-instrumentation hook attaches. Configure via the env file.
5932
+ import { fileURLToPath } from 'node:url'
5933
+ import path from 'node:path'
5934
+ import dotenv from 'dotenv'
5935
+
5936
+ const here = path.dirname(fileURLToPath(import.meta.url))
5937
+ dotenv.config({ path: path.join(here, '.env.neat') })
5938
+
5939
+ await import('@opentelemetry/auto-instrumentations-node/register')
5940
+ `;
5941
+ var OTEL_INIT_TS = `${OTEL_INIT_HEADER}
5942
+ // Loads .env.neat (OTEL_SERVICE_NAME, OTEL_EXPORTER_OTLP_ENDPOINT) before
5943
+ // the auto-instrumentation hook attaches. Configure via the env file.
5944
+ import { fileURLToPath } from 'node:url'
5945
+ import path from 'node:path'
5946
+ import dotenv from 'dotenv'
5947
+
5948
+ const here = path.dirname(fileURLToPath(import.meta.url))
5949
+ dotenv.config({ path: path.join(here, '.env.neat') })
5950
+
5951
+ await import('@opentelemetry/auto-instrumentations-node/register')
5952
+ `;
5953
+ function renderEnvNeat(serviceName) {
5954
+ return [
5955
+ "# Generated by `neat init --apply` (ADR-069).",
5956
+ `OTEL_SERVICE_NAME=${serviceName}`,
5957
+ "OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318",
5958
+ ""
5959
+ ].join("\n");
5960
+ }
5961
+
5962
+ // src/installers/javascript.ts
5900
5963
  var SDK_PACKAGES = [
5901
5964
  { name: "@opentelemetry/api", version: "^1.9.0" },
5902
5965
  { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
5903
- { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
5966
+ { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" },
5967
+ // ADR-069 §5 — dotenv is the fourth dep. The generated otel-init loads
5968
+ // .env.neat through it so OTEL_SERVICE_NAME and the endpoint are in scope
5969
+ // before the auto-instrumentation hook attaches.
5970
+ { name: "dotenv", version: "^16.4.5" }
5904
5971
  ];
5905
- var AUTO_INSTRUMENT_REQUIRE = "--require @opentelemetry/auto-instrumentations-node/register";
5906
5972
  var OTEL_ENV = {
5907
- // null targetNEAT does not write `.env` itself; the user sets the env
5908
- // var in their orchestration layer.
5973
+ // ADR-069 §4endpoint moves into the per-package .env.neat (written
5974
+ // by the apply phase). The envEdits surface stays for the dry-run
5975
+ // patch render: it documents the key/value the user can inspect in the
5976
+ // generated .env.neat.
5909
5977
  file: null,
5910
5978
  key: "OTEL_EXPORTER_OTLP_ENDPOINT",
5911
5979
  value: "http://localhost:4318"
@@ -5918,16 +5986,75 @@ async function readPackageJson(serviceDir) {
5918
5986
  return null;
5919
5987
  }
5920
5988
  }
5989
+ async function exists2(p) {
5990
+ try {
5991
+ await import_node_fs23.promises.stat(p);
5992
+ return true;
5993
+ } catch {
5994
+ return false;
5995
+ }
5996
+ }
5921
5997
  async function detect(serviceDir) {
5922
5998
  const pkg = await readPackageJson(serviceDir);
5923
5999
  return pkg !== null && typeof pkg.name === "string";
5924
6000
  }
5925
- function rewriteStartScript(start) {
5926
- if (start.includes(AUTO_INSTRUMENT_REQUIRE)) return start;
5927
- if (/^\s*node\b/.test(start)) {
5928
- return start.replace(/^\s*node\b\s*/, `node ${AUTO_INSTRUMENT_REQUIRE} `);
6001
+ var INDEX_CANDIDATES = ["index.ts", "index.tsx", "index.js", "index.mjs", "index.cjs"];
6002
+ async function resolveEntry(serviceDir, pkg) {
6003
+ if (typeof pkg.main === "string" && pkg.main.length > 0) {
6004
+ const candidate = import_node_path38.default.resolve(serviceDir, pkg.main);
6005
+ if (await exists2(candidate)) return candidate;
6006
+ }
6007
+ if (pkg.bin) {
6008
+ let binEntry;
6009
+ if (typeof pkg.bin === "string") {
6010
+ binEntry = pkg.bin;
6011
+ } else if (pkg.name && typeof pkg.bin[pkg.name] === "string") {
6012
+ binEntry = pkg.bin[pkg.name];
6013
+ } else {
6014
+ const first = Object.values(pkg.bin)[0];
6015
+ if (typeof first === "string") binEntry = first;
6016
+ }
6017
+ if (binEntry) {
6018
+ const candidate = import_node_path38.default.resolve(serviceDir, binEntry);
6019
+ if (await exists2(candidate)) return candidate;
6020
+ }
5929
6021
  }
5930
- return `node ${AUTO_INSTRUMENT_REQUIRE} -- ${start}`;
6022
+ for (const name of INDEX_CANDIDATES) {
6023
+ const candidate = import_node_path38.default.join(serviceDir, name);
6024
+ if (await exists2(candidate)) return candidate;
6025
+ }
6026
+ return null;
6027
+ }
6028
+ function dispatchEntry(entryFile, pkg) {
6029
+ const ext = import_node_path38.default.extname(entryFile).toLowerCase();
6030
+ if (ext === ".ts" || ext === ".tsx") return "ts";
6031
+ if (ext === ".mjs") return "esm";
6032
+ if (ext === ".cjs") return "cjs";
6033
+ return pkg.type === "module" ? "esm" : "cjs";
6034
+ }
6035
+ function otelInitFilename(flavor) {
6036
+ if (flavor === "ts") return "otel-init.ts";
6037
+ if (flavor === "esm") return "otel-init.mjs";
6038
+ return "otel-init.cjs";
6039
+ }
6040
+ function otelInitContents(flavor) {
6041
+ if (flavor === "ts") return OTEL_INIT_TS;
6042
+ if (flavor === "esm") return OTEL_INIT_ESM;
6043
+ return OTEL_INIT_CJS;
6044
+ }
6045
+ function injectionLine(flavor, entryFile, otelInitFile) {
6046
+ let rel = import_node_path38.default.relative(import_node_path38.default.dirname(entryFile), otelInitFile);
6047
+ if (!rel.startsWith(".")) rel = `./${rel}`;
6048
+ rel = rel.split(import_node_path38.default.sep).join("/");
6049
+ if (flavor === "cjs") return `require('${rel}')`;
6050
+ if (flavor === "esm") return `import '${rel}'`;
6051
+ const tsRel = rel.replace(/\.ts$/, "");
6052
+ return `import '${tsRel}'`;
6053
+ }
6054
+ function lineIsOtelInjection(line) {
6055
+ const trimmed = line.trim();
6056
+ if (trimmed.length === 0) return false;
6057
+ return /(?:require\(|import\s+)['"]\.\/otel-init[^'"]*['"]/.test(trimmed);
5931
6058
  }
5932
6059
  async function plan(serviceDir) {
5933
6060
  const pkg = await readPackageJson(serviceDir);
@@ -5937,9 +6064,17 @@ async function plan(serviceDir) {
5937
6064
  serviceDir,
5938
6065
  dependencyEdits: [],
5939
6066
  entrypointEdits: [],
5940
- envEdits: []
6067
+ envEdits: [],
6068
+ generatedFiles: []
5941
6069
  };
5942
6070
  if (!pkg) return empty;
6071
+ const entryFile = await resolveEntry(serviceDir, pkg);
6072
+ if (!entryFile) {
6073
+ return { ...empty, libOnly: true };
6074
+ }
6075
+ const flavor = dispatchEntry(entryFile, pkg);
6076
+ const otelInitFile = import_node_path38.default.join(import_node_path38.default.dirname(entryFile), otelInitFilename(flavor));
6077
+ const envNeatFile = import_node_path38.default.join(serviceDir, ".env.neat");
5943
6078
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5944
6079
  const dependencyEdits = [];
5945
6080
  for (const sdk of SDK_PACKAGES) {
@@ -5952,14 +6087,37 @@ async function plan(serviceDir) {
5952
6087
  });
5953
6088
  }
5954
6089
  const entrypointEdits = [];
5955
- const startScript = pkg.scripts?.start;
5956
- if (typeof startScript === "string" && startScript.trim().length > 0) {
5957
- const rewritten = rewriteStartScript(startScript);
5958
- if (rewritten !== startScript) {
5959
- entrypointEdits.push({ file: manifestPath, before: startScript, after: rewritten });
6090
+ try {
6091
+ const raw = await import_node_fs23.promises.readFile(entryFile, "utf8");
6092
+ const lines = raw.split(/\r?\n/);
6093
+ const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
6094
+ if (!lineIsOtelInjection(firstReal)) {
6095
+ const inject = injectionLine(flavor, entryFile, otelInitFile);
6096
+ entrypointEdits.push({
6097
+ file: entryFile,
6098
+ before: firstReal,
6099
+ after: inject
6100
+ });
5960
6101
  }
6102
+ } catch {
6103
+ return { ...empty, libOnly: true };
6104
+ }
6105
+ const generatedFiles = [];
6106
+ if (!await exists2(otelInitFile)) {
6107
+ generatedFiles.push({
6108
+ file: otelInitFile,
6109
+ contents: otelInitContents(flavor),
6110
+ skipIfExists: true
6111
+ });
5961
6112
  }
5962
- if (dependencyEdits.length === 0 && entrypointEdits.length === 0) {
6113
+ if (!await exists2(envNeatFile)) {
6114
+ generatedFiles.push({
6115
+ file: envNeatFile,
6116
+ contents: renderEnvNeat(pkg.name ?? import_node_path38.default.basename(serviceDir)),
6117
+ skipIfExists: true
6118
+ });
6119
+ }
6120
+ if (dependencyEdits.length === 0 && entrypointEdits.length === 0 && generatedFiles.length === 0) {
5963
6121
  return empty;
5964
6122
  }
5965
6123
  return {
@@ -5967,53 +6125,129 @@ async function plan(serviceDir) {
5967
6125
  serviceDir,
5968
6126
  dependencyEdits,
5969
6127
  entrypointEdits,
5970
- envEdits: [OTEL_ENV]
6128
+ envEdits: [OTEL_ENV],
6129
+ generatedFiles,
6130
+ entryFile,
6131
+ libOnly: false
5971
6132
  };
5972
6133
  }
6134
+ function isAllowedWritePath(serviceDir, target) {
6135
+ const rel = import_node_path38.default.relative(serviceDir, target);
6136
+ if (rel.startsWith("..")) return false;
6137
+ const base = import_node_path38.default.basename(target);
6138
+ if (base === "package.json") return true;
6139
+ if (base === ".env.neat") return true;
6140
+ if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
6141
+ return false;
6142
+ }
6143
+ async function writeAtomic(file, contents) {
6144
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
6145
+ await import_node_fs23.promises.writeFile(tmp, contents, "utf8");
6146
+ await import_node_fs23.promises.rename(tmp, file);
6147
+ }
5973
6148
  async function apply(installPlan) {
5974
- const touched = /* @__PURE__ */ new Set();
5975
- for (const e of installPlan.dependencyEdits) touched.add(e.file);
5976
- for (const e of installPlan.entrypointEdits) touched.add(e.file);
5977
- if (touched.size === 0) return;
6149
+ const { serviceDir } = installPlan;
6150
+ if (installPlan.libOnly) {
6151
+ return {
6152
+ serviceDir,
6153
+ outcome: "lib-only",
6154
+ reason: "no resolvable entry point",
6155
+ writtenFiles: []
6156
+ };
6157
+ }
6158
+ if (installPlan.dependencyEdits.length === 0 && installPlan.entrypointEdits.length === 0 && (installPlan.generatedFiles?.length ?? 0) === 0) {
6159
+ return {
6160
+ serviceDir,
6161
+ outcome: "already-instrumented",
6162
+ writtenFiles: []
6163
+ };
6164
+ }
6165
+ const allTargets = /* @__PURE__ */ new Set();
6166
+ for (const d of installPlan.dependencyEdits) allTargets.add(d.file);
6167
+ for (const e of installPlan.entrypointEdits) allTargets.add(e.file);
6168
+ for (const g of installPlan.generatedFiles ?? []) allTargets.add(g.file);
6169
+ for (const target of allTargets) {
6170
+ const isEntryEdit = installPlan.entrypointEdits.some((e) => e.file === target);
6171
+ if (isEntryEdit) continue;
6172
+ if (!isAllowedWritePath(serviceDir, target)) {
6173
+ throw new Error(
6174
+ `javascript installer: refusing to write outside the allowed path set (ADR-069 \xA77): ${target}`
6175
+ );
6176
+ }
6177
+ }
5978
6178
  const originals = /* @__PURE__ */ new Map();
5979
- for (const file of touched) {
5980
- try {
5981
- originals.set(file, await import_node_fs23.promises.readFile(file, "utf8"));
5982
- } catch {
6179
+ const createdFiles = [];
6180
+ for (const target of allTargets) {
6181
+ if (await exists2(target)) {
6182
+ try {
6183
+ originals.set(target, await import_node_fs23.promises.readFile(target, "utf8"));
6184
+ } catch {
6185
+ }
5983
6186
  }
5984
6187
  }
6188
+ const writtenFiles = [];
5985
6189
  try {
5986
- for (const file of touched) {
5987
- const raw = originals.get(file) ?? "";
6190
+ const manifestTargets = installPlan.dependencyEdits.reduce((acc, e) => {
6191
+ acc.add(e.file);
6192
+ return acc;
6193
+ }, /* @__PURE__ */ new Set());
6194
+ for (const file of manifestTargets) {
6195
+ const raw = originals.get(file);
6196
+ if (raw === void 0) {
6197
+ throw new Error(`javascript installer: cannot read ${file} during apply`);
6198
+ }
5988
6199
  const pkg = JSON.parse(raw);
5989
6200
  pkg.dependencies = pkg.dependencies ?? {};
5990
6201
  for (const dep of installPlan.dependencyEdits) {
5991
6202
  if (dep.file !== file) continue;
5992
6203
  if (dep.kind === "add") {
5993
- pkg.dependencies[dep.name] = dep.version;
6204
+ if (!(dep.name in (pkg.dependencies ?? {}))) {
6205
+ pkg.dependencies[dep.name] = dep.version;
6206
+ }
5994
6207
  } else {
5995
6208
  delete pkg.dependencies[dep.name];
5996
6209
  }
5997
6210
  }
5998
- for (const ep of installPlan.entrypointEdits) {
5999
- if (ep.file !== file) continue;
6000
- pkg.scripts = pkg.scripts ?? {};
6001
- if (pkg.scripts.start === ep.before) {
6002
- pkg.scripts.start = ep.after;
6003
- }
6004
- }
6005
6211
  const newRaw = JSON.stringify(pkg, null, 2) + "\n";
6006
- const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
6007
- await import_node_fs23.promises.writeFile(tmp, newRaw, "utf8");
6008
- await import_node_fs23.promises.rename(tmp, file);
6212
+ await writeAtomic(file, newRaw);
6213
+ writtenFiles.push(file);
6214
+ }
6215
+ for (const gen of installPlan.generatedFiles ?? []) {
6216
+ if (gen.skipIfExists && await exists2(gen.file)) {
6217
+ continue;
6218
+ }
6219
+ await writeAtomic(gen.file, gen.contents);
6220
+ if (!originals.has(gen.file)) createdFiles.push(gen.file);
6221
+ writtenFiles.push(gen.file);
6222
+ }
6223
+ for (const ep of installPlan.entrypointEdits) {
6224
+ const raw = originals.get(ep.file);
6225
+ if (raw === void 0) {
6226
+ throw new Error(`javascript installer: cannot read entry ${ep.file} during apply`);
6227
+ }
6228
+ const lines = raw.split(/\r?\n/);
6229
+ const hasShebang = lines[0]?.startsWith("#!") ?? false;
6230
+ const insertAt = hasShebang ? 1 : 0;
6231
+ const firstReal = lines[insertAt] ?? "";
6232
+ if (lineIsOtelInjection(firstReal)) continue;
6233
+ lines.splice(insertAt, 0, ep.after);
6234
+ const newRaw = lines.join("\n");
6235
+ await writeAtomic(ep.file, newRaw);
6236
+ writtenFiles.push(ep.file);
6009
6237
  }
6010
6238
  } catch (err) {
6011
- await rollback(installPlan, originals);
6239
+ await rollback(installPlan, originals, createdFiles);
6012
6240
  throw err;
6013
6241
  }
6242
+ return {
6243
+ serviceDir,
6244
+ outcome: "instrumented",
6245
+ writtenFiles
6246
+ };
6014
6247
  }
6015
- async function rollback(installPlan, originals) {
6248
+ async function rollback(installPlan, originals, createdFiles) {
6016
6249
  const restored = [];
6250
+ const removed = [];
6017
6251
  for (const [file, raw] of originals.entries()) {
6018
6252
  try {
6019
6253
  await import_node_fs23.promises.writeFile(file, raw, "utf8");
@@ -6021,6 +6255,13 @@ async function rollback(installPlan, originals) {
6021
6255
  } catch {
6022
6256
  }
6023
6257
  }
6258
+ for (const file of createdFiles) {
6259
+ try {
6260
+ await import_node_fs23.promises.unlink(file);
6261
+ removed.push(file);
6262
+ } catch {
6263
+ }
6264
+ }
6024
6265
  const lines = [
6025
6266
  "# neat-rollback.patch",
6026
6267
  "",
@@ -6028,6 +6269,7 @@ async function rollback(installPlan, originals) {
6028
6269
  "# Files listed below were restored to their pre-apply contents.",
6029
6270
  "",
6030
6271
  ...restored.map((f) => `restored: ${f}`),
6272
+ ...removed.map((f) => `removed: ${f}`),
6031
6273
  ""
6032
6274
  ];
6033
6275
  const rollbackPath = import_node_path38.default.join(installPlan.serviceDir, "neat-rollback.patch");
@@ -6053,7 +6295,7 @@ var OTEL_ENV2 = {
6053
6295
  key: "OTEL_EXPORTER_OTLP_ENDPOINT",
6054
6296
  value: "http://localhost:4318"
6055
6297
  };
6056
- async function exists2(p) {
6298
+ async function exists3(p) {
6057
6299
  try {
6058
6300
  await import_node_fs24.promises.stat(p);
6059
6301
  return true;
@@ -6064,7 +6306,7 @@ async function exists2(p) {
6064
6306
  async function detect2(serviceDir) {
6065
6307
  const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
6066
6308
  for (const m of markers) {
6067
- if (await exists2(import_node_path39.default.join(serviceDir, m))) return true;
6309
+ if (await exists3(import_node_path39.default.join(serviceDir, m))) return true;
6068
6310
  }
6069
6311
  return false;
6070
6312
  }
@@ -6075,7 +6317,7 @@ function reqPackageName(line) {
6075
6317
  }
6076
6318
  async function planRequirementsTxtEdits(serviceDir) {
6077
6319
  const file = import_node_path39.default.join(serviceDir, "requirements.txt");
6078
- if (!await exists2(file)) return null;
6320
+ if (!await exists3(file)) return null;
6079
6321
  const raw = await import_node_fs24.promises.readFile(file, "utf8");
6080
6322
  const presentNames = new Set(
6081
6323
  raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
@@ -6085,7 +6327,7 @@ async function planRequirementsTxtEdits(serviceDir) {
6085
6327
  }
6086
6328
  async function planProcfileEdits(serviceDir) {
6087
6329
  const procfile = import_node_path39.default.join(serviceDir, "Procfile");
6088
- if (!await exists2(procfile)) return [];
6330
+ if (!await exists3(procfile)) return [];
6089
6331
  const raw = await import_node_fs24.promises.readFile(procfile, "utf8");
6090
6332
  const edits = [];
6091
6333
  for (const line of raw.split(/\r?\n/)) {
@@ -6152,10 +6394,13 @@ async function applyProcfile(procfile, edits, original) {
6152
6394
  await import_node_fs24.promises.rename(tmp, procfile);
6153
6395
  }
6154
6396
  async function apply2(installPlan) {
6397
+ const { serviceDir } = installPlan;
6155
6398
  const touched = /* @__PURE__ */ new Set();
6156
6399
  for (const e of installPlan.dependencyEdits) touched.add(e.file);
6157
6400
  for (const e of installPlan.entrypointEdits) touched.add(e.file);
6158
- if (touched.size === 0) return;
6401
+ if (touched.size === 0) {
6402
+ return { serviceDir, outcome: "already-instrumented", writtenFiles: [] };
6403
+ }
6159
6404
  const originals = /* @__PURE__ */ new Map();
6160
6405
  for (const file of touched) {
6161
6406
  try {
@@ -6163,6 +6408,7 @@ async function apply2(installPlan) {
6163
6408
  } catch {
6164
6409
  }
6165
6410
  }
6411
+ const writtenFiles = [];
6166
6412
  try {
6167
6413
  for (const file of touched) {
6168
6414
  const raw = originals.get(file);
@@ -6172,16 +6418,23 @@ async function apply2(installPlan) {
6172
6418
  const base = import_node_path39.default.basename(file);
6173
6419
  if (base === "requirements.txt") {
6174
6420
  const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
6175
- if (edits.length > 0) await applyRequirementsTxt(file, edits, raw);
6421
+ if (edits.length > 0) {
6422
+ await applyRequirementsTxt(file, edits, raw);
6423
+ writtenFiles.push(file);
6424
+ }
6176
6425
  } else if (base === "Procfile") {
6177
6426
  const edits = installPlan.entrypointEdits.filter((e) => e.file === file);
6178
- if (edits.length > 0) await applyProcfile(file, edits, raw);
6427
+ if (edits.length > 0) {
6428
+ await applyProcfile(file, edits, raw);
6429
+ writtenFiles.push(file);
6430
+ }
6179
6431
  }
6180
6432
  }
6181
6433
  } catch (err) {
6182
6434
  await rollback2(installPlan, originals);
6183
6435
  throw err;
6184
6436
  }
6437
+ return { serviceDir, outcome: "instrumented", writtenFiles };
6185
6438
  }
6186
6439
  async function rollback2(installPlan, originals) {
6187
6440
  const restored = [];
@@ -6214,7 +6467,7 @@ var pythonInstaller = {
6214
6467
  // src/installers/shared.ts
6215
6468
  init_cjs_shims();
6216
6469
  function isEmptyPlan(plan3) {
6217
- return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0;
6470
+ return plan3.dependencyEdits.length === 0 && plan3.entrypointEdits.length === 0 && plan3.envEdits.length === 0 && (plan3.generatedFiles?.length ?? 0) === 0;
6218
6471
  }
6219
6472
 
6220
6473
  // src/installers/index.ts
@@ -6256,8 +6509,18 @@ function renderPatch(sections) {
6256
6509
  const { installer, plan: plan3 } = section;
6257
6510
  lines.push(`## ${installer} (${plan3.language}) \u2014 ${plan3.serviceDir}`);
6258
6511
  lines.push("");
6512
+ if (plan3.libOnly) {
6513
+ lines.push("### skipped \u2014 no resolvable entry point (lib-only)");
6514
+ lines.push("");
6515
+ continue;
6516
+ }
6517
+ if (plan3.entryFile) {
6518
+ lines.push(`entry: ${plan3.entryFile}`);
6519
+ lines.push("");
6520
+ }
6259
6521
  if (plan3.dependencyEdits.length > 0) {
6260
6522
  lines.push("### dependencies");
6523
+ const byFile = /* @__PURE__ */ new Map();
6261
6524
  for (const dep of plan3.dependencyEdits) {
6262
6525
  const base = dep.file.split(/[\\/]/).pop() ?? dep.file;
6263
6526
  if (FORBIDDEN_LOCKFILES.has(base)) {
@@ -6265,24 +6528,41 @@ function renderPatch(sections) {
6265
6528
  `installer "${installer}" produced a dependency edit against a lockfile (${dep.file}); lockfiles must never be touched (ADR-047).`
6266
6529
  );
6267
6530
  }
6268
- lines.push(`- ${dep.kind} ${dep.name}@${dep.version} in ${dep.file}`);
6531
+ const existing = byFile.get(dep.file) ?? [];
6532
+ existing.push(dep);
6533
+ byFile.set(dep.file, existing);
6534
+ }
6535
+ for (const [file, deps] of byFile) {
6536
+ lines.push(`--- ${file}`);
6537
+ for (const dep of deps) {
6538
+ lines.push(`+ "${dep.name}": "${dep.version}"`);
6539
+ }
6540
+ }
6541
+ lines.push("");
6542
+ }
6543
+ if (plan3.generatedFiles && plan3.generatedFiles.length > 0) {
6544
+ lines.push("### generated files");
6545
+ for (const gen of plan3.generatedFiles) {
6546
+ lines.push(`--- (new file) ${gen.file}`);
6547
+ for (const ln of gen.contents.split(/\r?\n/)) {
6548
+ lines.push(`+ ${ln}`);
6549
+ }
6269
6550
  }
6270
6551
  lines.push("");
6271
6552
  }
6272
6553
  if (plan3.entrypointEdits.length > 0) {
6273
- lines.push("### entrypoint");
6554
+ lines.push("### entry-point injection");
6274
6555
  for (const e of plan3.entrypointEdits) {
6275
- lines.push(`- ${e.file}`);
6276
- lines.push(` - before: ${e.before}`);
6277
- lines.push(` - after: ${e.after}`);
6556
+ lines.push(`--- ${e.file}`);
6557
+ lines.push(`+ ${e.after}`);
6558
+ lines.push(` ${e.before}`);
6278
6559
  }
6279
6560
  lines.push("");
6280
6561
  }
6281
6562
  if (plan3.envEdits.length > 0) {
6282
- lines.push("### env");
6563
+ lines.push("### env (written to <package-dir>/.env.neat)");
6283
6564
  for (const env of plan3.envEdits) {
6284
- const target = env.file ?? "(set in your orchestration layer)";
6285
- lines.push(`- ${env.key}=${env.value} \u2192 ${target}`);
6565
+ lines.push(`- ${env.key}=${env.value}`);
6286
6566
  }
6287
6567
  lines.push("");
6288
6568
  }
@@ -6291,11 +6571,11 @@ function renderPatch(sections) {
6291
6571
  }
6292
6572
 
6293
6573
  // src/cli.ts
6294
- var import_types23 = require("@neat.is/types");
6574
+ var import_types24 = require("@neat.is/types");
6295
6575
 
6296
6576
  // src/cli-client.ts
6297
6577
  init_cjs_shims();
6298
- var import_types22 = require("@neat.is/types");
6578
+ var import_types23 = require("@neat.is/types");
6299
6579
  var HttpError = class extends Error {
6300
6580
  constructor(status2, message, responseBody = "") {
6301
6581
  super(message);
@@ -6430,7 +6710,7 @@ async function runBlastRadius(client, input) {
6430
6710
  }
6431
6711
  }
6432
6712
  function formatBlastEntry(n) {
6433
- const tag = n.edgeProvenance === import_types22.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
6713
+ const tag = n.edgeProvenance === import_types23.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
6434
6714
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
6435
6715
  }
6436
6716
  async function runDependencies(client, input) {
@@ -6476,9 +6756,9 @@ async function runObservedDependencies(client, input) {
6476
6756
  const edges = await client.get(
6477
6757
  projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
6478
6758
  );
6479
- const observed = edges.outbound.filter((e) => e.provenance === import_types22.Provenance.OBSERVED);
6759
+ const observed = edges.outbound.filter((e) => e.provenance === import_types23.Provenance.OBSERVED);
6480
6760
  if (observed.length === 0) {
6481
- const hasExtracted = edges.outbound.some((e) => e.provenance === import_types22.Provenance.EXTRACTED);
6761
+ const hasExtracted = edges.outbound.some((e) => e.provenance === import_types23.Provenance.EXTRACTED);
6482
6762
  const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
6483
6763
  return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
6484
6764
  }
@@ -6486,7 +6766,7 @@ async function runObservedDependencies(client, input) {
6486
6766
  return {
6487
6767
  summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
6488
6768
  block: blockLines.join("\n"),
6489
- provenance: import_types22.Provenance.OBSERVED
6769
+ provenance: import_types23.Provenance.OBSERVED
6490
6770
  };
6491
6771
  } catch (err) {
6492
6772
  if (err instanceof HttpError && err.status === 404) {
@@ -6540,7 +6820,7 @@ async function runIncidents(client, input) {
6540
6820
  return {
6541
6821
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
6542
6822
  block: blockLines.join("\n"),
6543
- provenance: import_types22.Provenance.OBSERVED
6823
+ provenance: import_types23.Provenance.OBSERVED
6544
6824
  };
6545
6825
  } catch (err) {
6546
6826
  if (err instanceof HttpError && err.status === 404) {
@@ -6649,7 +6929,7 @@ async function runStaleEdges(client, input) {
6649
6929
  return {
6650
6930
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
6651
6931
  block: blockLines.join("\n"),
6652
- provenance: import_types22.Provenance.STALE
6932
+ provenance: import_types23.Provenance.STALE
6653
6933
  };
6654
6934
  }
6655
6935
  async function runPolicies(client, input) {
@@ -7031,7 +7311,7 @@ async function buildPatchSections(services) {
7031
7311
  const installer = await pickInstaller(svc.dir);
7032
7312
  if (!installer) continue;
7033
7313
  const plan3 = await installer.plan(svc.dir);
7034
- if (isEmptyPlan(plan3)) continue;
7314
+ if (isEmptyPlan(plan3) && !plan3.libOnly) continue;
7035
7315
  sections.push({ installer: installer.name, plan: plan3 });
7036
7316
  }
7037
7317
  return sections;
@@ -7084,14 +7364,28 @@ async function runInit(opts) {
7084
7364
  }
7085
7365
  if (!opts.noInstall) {
7086
7366
  if (opts.apply) {
7367
+ let instrumented = 0;
7368
+ let alreadyInstrumented = 0;
7369
+ let libOnly = 0;
7087
7370
  for (const section of sections) {
7088
7371
  const installer = INSTALLERS.find((i) => i.name === section.installer);
7089
7372
  if (!installer) continue;
7090
- await installer.apply(section.plan);
7373
+ const outcome = await installer.apply(section.plan);
7374
+ if (outcome.outcome === "instrumented") {
7375
+ instrumented++;
7376
+ for (const f of outcome.writtenFiles) written.push(f);
7377
+ } else if (outcome.outcome === "already-instrumented") {
7378
+ alreadyInstrumented++;
7379
+ } else if (outcome.outcome === "lib-only") {
7380
+ libOnly++;
7381
+ }
7091
7382
  }
7092
7383
  if (sections.length > 0) {
7093
7384
  console.log("");
7094
- console.log("patch applied. Run `npm install` (or your language equivalent) to refresh lockfiles.");
7385
+ console.log(
7386
+ `apply: instrumented ${instrumented}, already-instrumented ${alreadyInstrumented}, lib-only ${libOnly}`
7387
+ );
7388
+ console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
7095
7389
  }
7096
7390
  } else {
7097
7391
  await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");
@@ -7483,10 +7777,10 @@ async function runQueryVerb(cmd, parsed) {
7483
7777
  const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
7484
7778
  const out = [];
7485
7779
  for (const p of parts) {
7486
- const r = import_types23.DivergenceTypeSchema.safeParse(p);
7780
+ const r = import_types24.DivergenceTypeSchema.safeParse(p);
7487
7781
  if (!r.success) {
7488
7782
  console.error(
7489
- `neat divergences: unknown --type "${p}". allowed: ${import_types23.DivergenceTypeSchema.options.join(", ")}`
7783
+ `neat divergences: unknown --type "${p}". allowed: ${import_types24.DivergenceTypeSchema.options.join(", ")}`
7490
7784
  );
7491
7785
  return 2;
7492
7786
  }