@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/{chunk-T3A2GK3X.js → chunk-YHQYHFI3.js} +2 -2
- package/dist/{chunk-33ZZ2ZID.js → chunk-ZU2RQRCN.js} +94 -83
- package/dist/chunk-ZU2RQRCN.js.map +1 -0
- package/dist/cli.cjs +433 -139
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +339 -60
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +84 -70
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +165 -70
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +82 -2
- package/dist/neatd.js.map +1 -1
- package/dist/server.cjs +57 -45
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-33ZZ2ZID.js.map +0 -1
- /package/dist/{chunk-T3A2GK3X.js.map → chunk-YHQYHFI3.js.map} +0 -0
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.
|
|
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.
|
|
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 ??
|
|
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
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
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 ??
|
|
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
|
|
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
|
|
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
|
|
4266
|
+
var import_types22 = require("@neat.is/types");
|
|
4250
4267
|
|
|
4251
4268
|
// src/divergences.ts
|
|
4252
4269
|
init_cjs_shims();
|
|
4253
|
-
var
|
|
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,
|
|
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
|
|
4283
|
+
case import_types20.Provenance.EXTRACTED:
|
|
4267
4284
|
cur.extracted = e;
|
|
4268
4285
|
break;
|
|
4269
|
-
case
|
|
4286
|
+
case import_types20.Provenance.OBSERVED:
|
|
4270
4287
|
cur.observed = e;
|
|
4271
4288
|
break;
|
|
4272
|
-
case
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 !==
|
|
4363
|
-
if (edge.provenance !==
|
|
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 !==
|
|
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 !==
|
|
4388
|
-
if (edge.provenance !==
|
|
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 !==
|
|
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 !==
|
|
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
|
|
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
|
|
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
|
|
4718
|
+
return import_types21.RegistryFileSchema.parse(parsed);
|
|
4705
4719
|
}
|
|
4706
4720
|
async function writeRegistry(reg) {
|
|
4707
|
-
const validated =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
-
//
|
|
5908
|
-
//
|
|
5973
|
+
// ADR-069 §4 — endpoint 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
|
-
|
|
5926
|
-
|
|
5927
|
-
if (
|
|
5928
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5956
|
-
|
|
5957
|
-
const
|
|
5958
|
-
|
|
5959
|
-
|
|
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 (
|
|
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
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
|
|
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
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
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
|
-
|
|
5987
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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("###
|
|
6554
|
+
lines.push("### entry-point injection");
|
|
6274
6555
|
for (const e of plan3.entrypointEdits) {
|
|
6275
|
-
lines.push(
|
|
6276
|
-
lines.push(
|
|
6277
|
-
lines.push(`
|
|
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
|
-
|
|
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
|
|
6574
|
+
var import_types24 = require("@neat.is/types");
|
|
6295
6575
|
|
|
6296
6576
|
// src/cli-client.ts
|
|
6297
6577
|
init_cjs_shims();
|
|
6298
|
-
var
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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:
|
|
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:
|
|
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:
|
|
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(
|
|
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 =
|
|
7780
|
+
const r = import_types24.DivergenceTypeSchema.safeParse(p);
|
|
7487
7781
|
if (!r.success) {
|
|
7488
7782
|
console.error(
|
|
7489
|
-
`neat divergences: unknown --type "${p}". allowed: ${
|
|
7783
|
+
`neat divergences: unknown --type "${p}". allowed: ${import_types24.DivergenceTypeSchema.options.join(", ")}`
|
|
7490
7784
|
);
|
|
7491
7785
|
return 2;
|
|
7492
7786
|
}
|