@neat.is/core 0.4.15 → 0.4.17

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.
@@ -454,19 +454,19 @@ function confidenceFromMix(edges, now = Date.now()) {
454
454
  function longestIncomingWalk(graph, start, maxDepth) {
455
455
  let best = { path: [start], edges: [] };
456
456
  const visited = /* @__PURE__ */ new Set([start]);
457
- function step(node, path38, edges) {
458
- if (path38.length > best.path.length) {
459
- best = { path: [...path38], edges: [...edges] };
457
+ function step(node, path39, edges) {
458
+ if (path39.length > best.path.length) {
459
+ best = { path: [...path39], edges: [...edges] };
460
460
  }
461
- if (path38.length - 1 >= maxDepth) return;
461
+ if (path39.length - 1 >= maxDepth) return;
462
462
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
463
463
  for (const [srcId, edge] of incoming) {
464
464
  if (visited.has(srcId)) continue;
465
465
  visited.add(srcId);
466
- path38.push(srcId);
466
+ path39.push(srcId);
467
467
  edges.push(edge);
468
- step(srcId, path38, edges);
469
- path38.pop();
468
+ step(srcId, path39, edges);
469
+ path39.pop();
470
470
  edges.pop();
471
471
  visited.delete(srcId);
472
472
  }
@@ -1093,6 +1093,41 @@ function thresholdForEdgeType(edgeType, overrides) {
1093
1093
  const map = overrides ?? loadStaleThresholdsFromEnv();
1094
1094
  return map[edgeType] ?? FALLBACK_STALE_THRESHOLD_MS;
1095
1095
  }
1096
+ var DEFAULT_INCIDENT_THRESHOLDS = {
1097
+ threshold: 5,
1098
+ windowMs: 6e4
1099
+ };
1100
+ function loadIncidentThresholdsFromEnv() {
1101
+ const raw = process.env.NEAT_INCIDENT_THRESHOLDS;
1102
+ if (!raw) return DEFAULT_INCIDENT_THRESHOLDS;
1103
+ try {
1104
+ const overrides = JSON.parse(raw);
1105
+ const merged = { ...DEFAULT_INCIDENT_THRESHOLDS };
1106
+ if (typeof overrides.threshold === "number" && Number.isFinite(overrides.threshold) && overrides.threshold >= 1) {
1107
+ merged.threshold = Math.floor(overrides.threshold);
1108
+ }
1109
+ if (typeof overrides.windowMs === "number" && Number.isFinite(overrides.windowMs) && overrides.windowMs >= 0) {
1110
+ merged.windowMs = overrides.windowMs;
1111
+ }
1112
+ return merged;
1113
+ } catch (err) {
1114
+ console.warn(
1115
+ `[neat] NEAT_INCIDENT_THRESHOLDS could not be parsed (${err.message}); using defaults`
1116
+ );
1117
+ return DEFAULT_INCIDENT_THRESHOLDS;
1118
+ }
1119
+ }
1120
+ function httpResponseStatus(span) {
1121
+ for (const key of ["http.response.status_code", "http.status_code"]) {
1122
+ const v = span.attributes[key];
1123
+ if (typeof v === "number" && Number.isFinite(v)) return v;
1124
+ if (typeof v === "string") {
1125
+ const n = Number(v);
1126
+ if (Number.isFinite(n)) return n;
1127
+ }
1128
+ }
1129
+ return void 0;
1130
+ }
1096
1131
  function nowIso(ctx) {
1097
1132
  return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
1098
1133
  }
@@ -1506,6 +1541,71 @@ function makeErrorSpanWriter(errorsPath) {
1506
1541
  await fs3.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
1507
1542
  };
1508
1543
  }
1544
+ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
1545
+ const attrs = sanitizeAttributes(span.attributes);
1546
+ const first = firstTimestamp ?? timestamp;
1547
+ const peer = pickAddress(span);
1548
+ const message = count > 1 ? `${count} consecutive HTTP ${statusCode} responses` + (peer ? ` to ${peer}` : "") : `HTTP ${statusCode} response` + (peer ? ` from ${peer}` : "");
1549
+ const ev = {
1550
+ id: `${span.traceId}:${span.spanId}`,
1551
+ timestamp,
1552
+ service: span.service,
1553
+ traceId: span.traceId,
1554
+ spanId: span.spanId,
1555
+ errorType: "http-failure",
1556
+ errorMessage: message,
1557
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
1558
+ affectedNode,
1559
+ httpStatusCode: statusCode,
1560
+ incidentCount: count,
1561
+ firstTimestamp: first,
1562
+ lastTimestamp: timestamp
1563
+ };
1564
+ await appendErrorEvent(ctx, ev);
1565
+ }
1566
+ async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status) {
1567
+ const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
1568
+ if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
1569
+ const peer = pickAddress(span) ?? span.spanId;
1570
+ const key = `${span.service}->${peer}`;
1571
+ const existing = ctx.burstState.get(key);
1572
+ let state;
1573
+ if (existing && nowMs - existing.lastMs <= windowMs) {
1574
+ existing.count += 1;
1575
+ existing.lastTs = ts;
1576
+ existing.lastMs = nowMs;
1577
+ existing.codes.set(status, (existing.codes.get(status) ?? 0) + 1);
1578
+ state = existing;
1579
+ } else {
1580
+ state = {
1581
+ count: 1,
1582
+ firstTs: ts,
1583
+ lastTs: ts,
1584
+ lastMs: nowMs,
1585
+ codes: /* @__PURE__ */ new Map([[status, 1]])
1586
+ };
1587
+ ctx.burstState.set(key, state);
1588
+ }
1589
+ if (state.count < threshold) return;
1590
+ let dominant = status;
1591
+ let max = 0;
1592
+ for (const [code, n] of state.codes) {
1593
+ if (n > max) {
1594
+ max = n;
1595
+ dominant = code;
1596
+ }
1597
+ }
1598
+ await recordFailingResponseIncident(
1599
+ ctx,
1600
+ span,
1601
+ affectedNode,
1602
+ state.lastTs,
1603
+ dominant,
1604
+ state.count,
1605
+ state.firstTs
1606
+ );
1607
+ ctx.burstState.delete(key);
1608
+ }
1509
1609
  async function handleSpan(ctx, span) {
1510
1610
  const ts = span.startTimeIso ?? nowIso(ctx);
1511
1611
  const nowMs = ctx.now ? ctx.now() : Date.now();
@@ -1604,6 +1704,14 @@ async function handleSpan(ctx, span) {
1604
1704
  await appendErrorEvent(ctx, ev);
1605
1705
  }
1606
1706
  }
1707
+ if (span.statusCode !== 2) {
1708
+ const status = httpResponseStatus(span);
1709
+ if (status !== void 0 && status >= 500) {
1710
+ await recordFailingResponseIncident(ctx, span, sourceId, ts, status, 1);
1711
+ } else if (status !== void 0 && status >= 400 && spanMintsObservedEdge(span.kind)) {
1712
+ await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status);
1713
+ }
1714
+ }
1607
1715
  void affectedNode;
1608
1716
  if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
1609
1717
  }
@@ -3995,6 +4103,65 @@ function grpcEndpointsFromFile(file, serviceDir) {
3995
4103
  return out;
3996
4104
  }
3997
4105
 
4106
+ // src/extract/calls/supabase.ts
4107
+ import path27 from "path";
4108
+ import { infraId as infraId5 } from "@neat.is/types";
4109
+ var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
4110
+ var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
4111
+ var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
4112
+ function hostFromLiteral(literal) {
4113
+ if (!literal) return null;
4114
+ const m = /^https?:\/\/([^/:'"`\s]+)/.exec(literal.trim());
4115
+ if (!m) return null;
4116
+ const host = m[1];
4117
+ if (!/\.supabase\.(co|in)$/i.test(host)) return null;
4118
+ return host;
4119
+ }
4120
+ function readImports2(content) {
4121
+ return {
4122
+ hasSupabaseJs: SUPABASE_JS_IMPORT_RE.test(content),
4123
+ hasSupabaseSsr: SUPABASE_SSR_IMPORT_RE.test(content)
4124
+ };
4125
+ }
4126
+ function constructorMatchesImport(name, ctx) {
4127
+ if (name === "createClient") return ctx.hasSupabaseJs;
4128
+ return ctx.hasSupabaseSsr;
4129
+ }
4130
+ function supabaseEndpointsFromFile(file, serviceDir) {
4131
+ const ctx = readImports2(file.content);
4132
+ if (!ctx.hasSupabaseJs && !ctx.hasSupabaseSsr) return [];
4133
+ const out = [];
4134
+ const seen = /* @__PURE__ */ new Set();
4135
+ SUPABASE_CLIENT_RE.lastIndex = 0;
4136
+ let m;
4137
+ while ((m = SUPABASE_CLIENT_RE.exec(file.content)) !== null) {
4138
+ const ctor = m[1];
4139
+ if (!constructorMatchesImport(ctor, ctx)) continue;
4140
+ const host = hostFromLiteral(m[2]);
4141
+ const name = host ?? "env";
4142
+ if (seen.has(name)) continue;
4143
+ seen.add(name);
4144
+ const line = lineOf(file.content, m[0]);
4145
+ out.push({
4146
+ infraId: infraId5("supabase", name),
4147
+ name,
4148
+ kind: "supabase",
4149
+ edgeType: "CALLS",
4150
+ // `createClient(...)` from @supabase/supabase-js (or createServerClient /
4151
+ // createBrowserClient from @supabase/ssr) with the import in scope — a
4152
+ // framework-aware recognizer matched the SDK shape. Verified-call-site
4153
+ // tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
4154
+ confidenceKind: "verified-call-site",
4155
+ evidence: {
4156
+ file: path27.relative(serviceDir, file.path),
4157
+ line,
4158
+ snippet: snippet(file.content, line)
4159
+ }
4160
+ });
4161
+ }
4162
+ return out;
4163
+ }
4164
+
3998
4165
  // src/extract/calls/index.ts
3999
4166
  function edgeTypeFromEndpoint(ep) {
4000
4167
  switch (ep.edgeType) {
@@ -4023,6 +4190,7 @@ async function addExternalEndpointEdges(graph, services) {
4023
4190
  endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
4024
4191
  endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
4025
4192
  endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
4193
+ endpoints.push(...supabaseEndpointsFromFile(maskedFile, service.dir));
4026
4194
  }
4027
4195
  if (endpoints.length === 0) continue;
4028
4196
  const seenEdges = /* @__PURE__ */ new Set();
@@ -4093,14 +4261,14 @@ async function addCallEdges(graph, services) {
4093
4261
  }
4094
4262
 
4095
4263
  // src/extract/infra/docker-compose.ts
4096
- import path27 from "path";
4264
+ import path28 from "path";
4097
4265
  import { EdgeType as EdgeType10, Provenance as Provenance8, confidenceForExtracted as confidenceForExtracted7 } from "@neat.is/types";
4098
4266
 
4099
4267
  // src/extract/infra/shared.ts
4100
- import { NodeType as NodeType10, infraId as infraId5 } from "@neat.is/types";
4268
+ import { NodeType as NodeType10, infraId as infraId6 } from "@neat.is/types";
4101
4269
  function makeInfraNode(kind, name, provider = "self", extras) {
4102
4270
  return {
4103
- id: infraId5(kind, name),
4271
+ id: infraId6(kind, name),
4104
4272
  type: NodeType10.InfraNode,
4105
4273
  name,
4106
4274
  provider,
@@ -4130,7 +4298,7 @@ function dependsOnList(value) {
4130
4298
  }
4131
4299
  function serviceNameToServiceNode(name, services) {
4132
4300
  for (const s of services) {
4133
- if (s.node.name === name || path27.basename(s.dir) === name) return s.node.id;
4301
+ if (s.node.name === name || path28.basename(s.dir) === name) return s.node.id;
4134
4302
  }
4135
4303
  return null;
4136
4304
  }
@@ -4139,7 +4307,7 @@ async function addComposeInfra(graph, scanPath, services) {
4139
4307
  let edgesAdded = 0;
4140
4308
  let composePath = null;
4141
4309
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
4142
- const abs = path27.join(scanPath, name);
4310
+ const abs = path28.join(scanPath, name);
4143
4311
  if (await exists(abs)) {
4144
4312
  composePath = abs;
4145
4313
  break;
@@ -4152,13 +4320,13 @@ async function addComposeInfra(graph, scanPath, services) {
4152
4320
  } catch (err) {
4153
4321
  recordExtractionError(
4154
4322
  "infra docker-compose",
4155
- path27.relative(scanPath, composePath),
4323
+ path28.relative(scanPath, composePath),
4156
4324
  err
4157
4325
  );
4158
4326
  return { nodesAdded, edgesAdded };
4159
4327
  }
4160
4328
  if (!compose?.services) return { nodesAdded, edgesAdded };
4161
- const evidenceFile = path27.relative(scanPath, composePath).split(path27.sep).join("/");
4329
+ const evidenceFile = path28.relative(scanPath, composePath).split(path28.sep).join("/");
4162
4330
  const composeNameToNodeId = /* @__PURE__ */ new Map();
4163
4331
  for (const [composeName, svc] of Object.entries(compose.services)) {
4164
4332
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -4199,7 +4367,7 @@ async function addComposeInfra(graph, scanPath, services) {
4199
4367
  }
4200
4368
 
4201
4369
  // src/extract/infra/dockerfile.ts
4202
- import path28 from "path";
4370
+ import path29 from "path";
4203
4371
  import { promises as fs15 } from "fs";
4204
4372
  import { EdgeType as EdgeType11, Provenance as Provenance9, confidenceForExtracted as confidenceForExtracted8 } from "@neat.is/types";
4205
4373
  function runtimeImage(content) {
@@ -4220,7 +4388,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4220
4388
  let nodesAdded = 0;
4221
4389
  let edgesAdded = 0;
4222
4390
  for (const service of services) {
4223
- const dockerfilePath = path28.join(service.dir, "Dockerfile");
4391
+ const dockerfilePath = path29.join(service.dir, "Dockerfile");
4224
4392
  if (!await exists(dockerfilePath)) continue;
4225
4393
  let content;
4226
4394
  try {
@@ -4228,7 +4396,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4228
4396
  } catch (err) {
4229
4397
  recordExtractionError(
4230
4398
  "infra dockerfile",
4231
- path28.relative(scanPath, dockerfilePath),
4399
+ path29.relative(scanPath, dockerfilePath),
4232
4400
  err
4233
4401
  );
4234
4402
  continue;
@@ -4240,7 +4408,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4240
4408
  graph.addNode(node.id, node);
4241
4409
  nodesAdded++;
4242
4410
  }
4243
- const relDockerfile = toPosix2(path28.relative(service.dir, dockerfilePath));
4411
+ const relDockerfile = toPosix2(path29.relative(service.dir, dockerfilePath));
4244
4412
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4245
4413
  graph,
4246
4414
  service.pkg.name,
@@ -4259,7 +4427,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4259
4427
  provenance: Provenance9.EXTRACTED,
4260
4428
  confidence: confidenceForExtracted8("structural"),
4261
4429
  evidence: {
4262
- file: toPosix2(path28.relative(scanPath, dockerfilePath))
4430
+ file: toPosix2(path29.relative(scanPath, dockerfilePath))
4263
4431
  }
4264
4432
  };
4265
4433
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4271,7 +4439,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4271
4439
 
4272
4440
  // src/extract/infra/terraform.ts
4273
4441
  import { promises as fs16 } from "fs";
4274
- import path29 from "path";
4442
+ import path30 from "path";
4275
4443
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
4276
4444
  async function walkTfFiles(start, depth = 0, max = 5) {
4277
4445
  if (depth > max) return [];
@@ -4280,11 +4448,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
4280
4448
  for (const entry of entries) {
4281
4449
  if (entry.isDirectory()) {
4282
4450
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
4283
- const child = path29.join(start, entry.name);
4451
+ const child = path30.join(start, entry.name);
4284
4452
  if (await isPythonVenvDir(child)) continue;
4285
4453
  out.push(...await walkTfFiles(child, depth + 1, max));
4286
4454
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
4287
- out.push(path29.join(start, entry.name));
4455
+ out.push(path30.join(start, entry.name));
4288
4456
  }
4289
4457
  }
4290
4458
  return out;
@@ -4311,7 +4479,7 @@ async function addTerraformResources(graph, scanPath) {
4311
4479
 
4312
4480
  // src/extract/infra/k8s.ts
4313
4481
  import { promises as fs17 } from "fs";
4314
- import path30 from "path";
4482
+ import path31 from "path";
4315
4483
  import { parseAllDocuments as parseAllDocuments2 } from "yaml";
4316
4484
  var K8S_KIND_TO_INFRA_KIND = {
4317
4485
  Service: "k8s-service",
@@ -4329,11 +4497,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
4329
4497
  for (const entry of entries) {
4330
4498
  if (entry.isDirectory()) {
4331
4499
  if (IGNORED_DIRS.has(entry.name)) continue;
4332
- const child = path30.join(start, entry.name);
4500
+ const child = path31.join(start, entry.name);
4333
4501
  if (await isPythonVenvDir(child)) continue;
4334
4502
  out.push(...await walkYamlFiles2(child, depth + 1, max));
4335
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path30.extname(entry.name))) {
4336
- out.push(path30.join(start, entry.name));
4503
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path31.extname(entry.name))) {
4504
+ out.push(path31.join(start, entry.name));
4337
4505
  }
4338
4506
  }
4339
4507
  return out;
@@ -4377,11 +4545,11 @@ async function addInfra(graph, scanPath, services) {
4377
4545
  }
4378
4546
 
4379
4547
  // src/extract/index.ts
4380
- import path32 from "path";
4548
+ import path33 from "path";
4381
4549
 
4382
4550
  // src/extract/retire.ts
4383
4551
  import { existsSync as existsSync2 } from "fs";
4384
- import path31 from "path";
4552
+ import path32 from "path";
4385
4553
  import { NodeType as NodeType11, Provenance as Provenance10 } from "@neat.is/types";
4386
4554
  function dropOrphanedFileNodes(graph) {
4387
4555
  const orphans = [];
@@ -4415,11 +4583,11 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4415
4583
  if (edge.provenance !== Provenance10.EXTRACTED) return;
4416
4584
  const evidenceFile = edge.evidence?.file;
4417
4585
  if (!evidenceFile) return;
4418
- if (path31.isAbsolute(evidenceFile)) {
4586
+ if (path32.isAbsolute(evidenceFile)) {
4419
4587
  if (!existsSync2(evidenceFile)) toDrop.push(id);
4420
4588
  return;
4421
4589
  }
4422
- const found = bases.some((base) => existsSync2(path31.join(base, evidenceFile)));
4590
+ const found = bases.some((base) => existsSync2(path32.join(base, evidenceFile)));
4423
4591
  if (!found) toDrop.push(id);
4424
4592
  });
4425
4593
  for (const id of toDrop) graph.dropEdge(id);
@@ -4459,7 +4627,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4459
4627
  }
4460
4628
  const droppedEntries = drainDroppedExtracted();
4461
4629
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
4462
- const rejectedPath = path32.join(path32.dirname(opts.errorsPath), "rejected.ndjson");
4630
+ const rejectedPath = path33.join(path33.dirname(opts.errorsPath), "rejected.ndjson");
4463
4631
  try {
4464
4632
  await writeRejectedExtracted(droppedEntries, rejectedPath);
4465
4633
  } catch (err) {
@@ -4736,7 +4904,7 @@ function computeDivergences(graph, opts = {}) {
4736
4904
 
4737
4905
  // src/persist.ts
4738
4906
  import { promises as fs18 } from "fs";
4739
- import path33 from "path";
4907
+ import path34 from "path";
4740
4908
  import { Provenance as Provenance12, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
4741
4909
  var SCHEMA_VERSION = 4;
4742
4910
  function migrateV1ToV2(payload) {
@@ -4773,7 +4941,7 @@ function migrateV2ToV3(payload) {
4773
4941
  return { ...payload, schemaVersion: 3 };
4774
4942
  }
4775
4943
  async function ensureDir(filePath) {
4776
- await fs18.mkdir(path33.dirname(filePath), { recursive: true });
4944
+ await fs18.mkdir(path34.dirname(filePath), { recursive: true });
4777
4945
  }
4778
4946
  async function saveGraphToDisk(graph, outPath) {
4779
4947
  await ensureDir(outPath);
@@ -4923,23 +5091,23 @@ function canonicalJson(value) {
4923
5091
  }
4924
5092
 
4925
5093
  // src/projects.ts
4926
- import path34 from "path";
5094
+ import path35 from "path";
4927
5095
  function pathsForProject(project, baseDir) {
4928
5096
  if (project === DEFAULT_PROJECT) {
4929
5097
  return {
4930
- snapshotPath: path34.join(baseDir, "graph.json"),
4931
- errorsPath: path34.join(baseDir, "errors.ndjson"),
4932
- staleEventsPath: path34.join(baseDir, "stale-events.ndjson"),
4933
- embeddingsCachePath: path34.join(baseDir, "embeddings.json"),
4934
- policyViolationsPath: path34.join(baseDir, "policy-violations.ndjson")
5098
+ snapshotPath: path35.join(baseDir, "graph.json"),
5099
+ errorsPath: path35.join(baseDir, "errors.ndjson"),
5100
+ staleEventsPath: path35.join(baseDir, "stale-events.ndjson"),
5101
+ embeddingsCachePath: path35.join(baseDir, "embeddings.json"),
5102
+ policyViolationsPath: path35.join(baseDir, "policy-violations.ndjson")
4935
5103
  };
4936
5104
  }
4937
5105
  return {
4938
- snapshotPath: path34.join(baseDir, `${project}.json`),
4939
- errorsPath: path34.join(baseDir, `errors.${project}.ndjson`),
4940
- staleEventsPath: path34.join(baseDir, `stale-events.${project}.ndjson`),
4941
- embeddingsCachePath: path34.join(baseDir, `embeddings.${project}.json`),
4942
- policyViolationsPath: path34.join(baseDir, `policy-violations.${project}.ndjson`)
5106
+ snapshotPath: path35.join(baseDir, `${project}.json`),
5107
+ errorsPath: path35.join(baseDir, `errors.${project}.ndjson`),
5108
+ staleEventsPath: path35.join(baseDir, `stale-events.${project}.ndjson`),
5109
+ embeddingsCachePath: path35.join(baseDir, `embeddings.${project}.json`),
5110
+ policyViolationsPath: path35.join(baseDir, `policy-violations.${project}.ndjson`)
4943
5111
  };
4944
5112
  }
4945
5113
  var Projects = class {
@@ -4980,7 +5148,7 @@ function parseExtraProjects(raw) {
4980
5148
  // src/registry.ts
4981
5149
  import { promises as fs20 } from "fs";
4982
5150
  import os2 from "os";
4983
- import path35 from "path";
5151
+ import path36 from "path";
4984
5152
  import {
4985
5153
  RegistryFileSchema
4986
5154
  } from "@neat.is/types";
@@ -4988,17 +5156,17 @@ var LOCK_TIMEOUT_MS = 5e3;
4988
5156
  var LOCK_RETRY_MS = 50;
4989
5157
  function neatHome() {
4990
5158
  const override = process.env.NEAT_HOME;
4991
- if (override && override.length > 0) return path35.resolve(override);
4992
- return path35.join(os2.homedir(), ".neat");
5159
+ if (override && override.length > 0) return path36.resolve(override);
5160
+ return path36.join(os2.homedir(), ".neat");
4993
5161
  }
4994
5162
  function registryPath() {
4995
- return path35.join(neatHome(), "projects.json");
5163
+ return path36.join(neatHome(), "projects.json");
4996
5164
  }
4997
5165
  function registryLockPath() {
4998
- return path35.join(neatHome(), "projects.json.lock");
5166
+ return path36.join(neatHome(), "projects.json.lock");
4999
5167
  }
5000
5168
  function daemonPidPath() {
5001
- return path35.join(neatHome(), "neatd.pid");
5169
+ return path36.join(neatHome(), "neatd.pid");
5002
5170
  }
5003
5171
  function isPidAliveDefault(pid) {
5004
5172
  try {
@@ -5058,7 +5226,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
5058
5226
  }
5059
5227
  }
5060
5228
  async function normalizeProjectPath(input) {
5061
- const resolved = path35.resolve(input);
5229
+ const resolved = path36.resolve(input);
5062
5230
  try {
5063
5231
  return await fs20.realpath(resolved);
5064
5232
  } catch {
@@ -5066,7 +5234,7 @@ async function normalizeProjectPath(input) {
5066
5234
  }
5067
5235
  }
5068
5236
  async function writeAtomically(target, contents) {
5069
- await fs20.mkdir(path35.dirname(target), { recursive: true });
5237
+ await fs20.mkdir(path36.dirname(target), { recursive: true });
5070
5238
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
5071
5239
  const fd = await fs20.open(tmp, "w");
5072
5240
  try {
@@ -5079,7 +5247,7 @@ async function writeAtomically(target, contents) {
5079
5247
  }
5080
5248
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
5081
5249
  const deadline = Date.now() + timeoutMs;
5082
- await fs20.mkdir(path35.dirname(lockPath), { recursive: true });
5250
+ await fs20.mkdir(path36.dirname(lockPath), { recursive: true });
5083
5251
  let probedHolder = false;
5084
5252
  while (true) {
5085
5253
  try {
@@ -5215,6 +5383,58 @@ async function removeProject(name) {
5215
5383
  return removed;
5216
5384
  });
5217
5385
  }
5386
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
5387
+ var DEFAULT_PRUNE_TTL_MS = 7 * DAY_MS2;
5388
+ function pruneTtlMs() {
5389
+ const raw = process.env.NEAT_REGISTRY_PRUNE_TTL_MS;
5390
+ if (!raw) return DEFAULT_PRUNE_TTL_MS;
5391
+ const n = Number.parseInt(raw, 10);
5392
+ if (Number.isFinite(n) && n >= 0) return n;
5393
+ console.warn(
5394
+ `[neat] NEAT_REGISTRY_PRUNE_TTL_MS could not be parsed (${raw}); using default ${DEFAULT_PRUNE_TTL_MS}ms`
5395
+ );
5396
+ return DEFAULT_PRUNE_TTL_MS;
5397
+ }
5398
+ async function statPathStatus(p) {
5399
+ try {
5400
+ const stat = await fs20.stat(p);
5401
+ return stat.isDirectory() ? "present" : "unknown";
5402
+ } catch (err) {
5403
+ return err.code === "ENOENT" ? "gone" : "unknown";
5404
+ }
5405
+ }
5406
+ async function pruneRegistry(opts = {}) {
5407
+ const ttlMs = opts.ttlMs ?? pruneTtlMs();
5408
+ const statPath = opts.statPath ?? statPathStatus;
5409
+ const now = opts.now ?? Date.now;
5410
+ return withLock(async () => {
5411
+ const reg = await readRegistry();
5412
+ const removed = [];
5413
+ const kept = [];
5414
+ for (const entry of reg.projects) {
5415
+ const status = await statPath(entry.path);
5416
+ if (status !== "gone") {
5417
+ kept.push(entry);
5418
+ continue;
5419
+ }
5420
+ if (ttlMs <= 0) {
5421
+ removed.push(entry);
5422
+ continue;
5423
+ }
5424
+ const lastSeen = Date.parse(entry.lastSeenAt ?? entry.registeredAt);
5425
+ const age = now() - (Number.isFinite(lastSeen) ? lastSeen : 0);
5426
+ if (age > ttlMs) {
5427
+ removed.push(entry);
5428
+ } else {
5429
+ kept.push(entry);
5430
+ }
5431
+ }
5432
+ if (removed.length === 0) return [];
5433
+ reg.projects = kept;
5434
+ await writeRegistry(reg);
5435
+ return removed;
5436
+ });
5437
+ }
5218
5438
 
5219
5439
  // src/api.ts
5220
5440
  import Fastify from "fastify";
@@ -5223,13 +5443,13 @@ import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } f
5223
5443
 
5224
5444
  // src/extend/index.ts
5225
5445
  import { promises as fs22 } from "fs";
5226
- import path37 from "path";
5446
+ import path38 from "path";
5227
5447
  import os3 from "os";
5228
5448
  import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
5229
5449
 
5230
5450
  // src/installers/package-manager.ts
5231
5451
  import { promises as fs21 } from "fs";
5232
- import path36 from "path";
5452
+ import path37 from "path";
5233
5453
  import { spawn } from "child_process";
5234
5454
  var LOCKFILE_PRIORITY = [
5235
5455
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -5251,22 +5471,22 @@ async function exists2(p) {
5251
5471
  }
5252
5472
  }
5253
5473
  async function detectPackageManager(serviceDir) {
5254
- let dir = path36.resolve(serviceDir);
5474
+ let dir = path37.resolve(serviceDir);
5255
5475
  const stops = /* @__PURE__ */ new Set();
5256
5476
  for (let i = 0; i < 64; i++) {
5257
5477
  if (stops.has(dir)) break;
5258
5478
  stops.add(dir);
5259
5479
  for (const candidate of LOCKFILE_PRIORITY) {
5260
- const lockPath = path36.join(dir, candidate.lockfile);
5480
+ const lockPath = path37.join(dir, candidate.lockfile);
5261
5481
  if (await exists2(lockPath)) {
5262
5482
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
5263
5483
  }
5264
5484
  }
5265
- const parent = path36.dirname(dir);
5485
+ const parent = path37.dirname(dir);
5266
5486
  if (parent === dir) break;
5267
5487
  dir = parent;
5268
5488
  }
5269
- return { pm: "npm", cwd: path36.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5489
+ return { pm: "npm", cwd: path37.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5270
5490
  }
5271
5491
  async function runPackageManagerInstall(cmd) {
5272
5492
  return new Promise((resolve) => {
@@ -5315,7 +5535,7 @@ async function fileExists2(p) {
5315
5535
  }
5316
5536
  }
5317
5537
  async function readPackageJson(scanPath) {
5318
- const pkgPath = path37.join(scanPath, "package.json");
5538
+ const pkgPath = path38.join(scanPath, "package.json");
5319
5539
  const raw = await fs22.readFile(pkgPath, "utf8");
5320
5540
  return JSON.parse(raw);
5321
5541
  }
@@ -5326,11 +5546,11 @@ async function findHookFiles(scanPath) {
5326
5546
  ).sort();
5327
5547
  }
5328
5548
  function extendLogPath() {
5329
- return process.env.NEAT_EXTEND_LOG ?? path37.join(os3.homedir(), ".neat", "extend-log.ndjson");
5549
+ return process.env.NEAT_EXTEND_LOG ?? path38.join(os3.homedir(), ".neat", "extend-log.ndjson");
5330
5550
  }
5331
5551
  async function appendExtendLog(entry) {
5332
5552
  const logPath = extendLogPath();
5333
- await fs22.mkdir(path37.dirname(logPath), { recursive: true });
5553
+ await fs22.mkdir(path38.dirname(logPath), { recursive: true });
5334
5554
  await fs22.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
5335
5555
  }
5336
5556
  function splicedContent(fileContent, snippet2) {
@@ -5389,7 +5609,7 @@ function lookupInstrumentation(library, installedVersion) {
5389
5609
  }
5390
5610
  async function describeProjectInstrumentation(ctx) {
5391
5611
  const hookFiles = await findHookFiles(ctx.scanPath);
5392
- const envNeat = await fileExists2(path37.join(ctx.scanPath, ".env.neat"));
5612
+ const envNeat = await fileExists2(path38.join(ctx.scanPath, ".env.neat"));
5393
5613
  const registryInstrPackages = new Set(
5394
5614
  registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
5395
5615
  );
@@ -5411,16 +5631,16 @@ async function applyExtension(ctx, args, options) {
5411
5631
  );
5412
5632
  }
5413
5633
  for (const file of hookFiles) {
5414
- const content = await fs22.readFile(path37.join(ctx.scanPath, file), "utf8");
5634
+ const content = await fs22.readFile(path38.join(ctx.scanPath, file), "utf8");
5415
5635
  if (content.includes(args.registration_snippet)) {
5416
5636
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
5417
5637
  }
5418
5638
  }
5419
5639
  const primaryFile = hookFiles[0];
5420
- const primaryPath = path37.join(ctx.scanPath, primaryFile);
5640
+ const primaryPath = path38.join(ctx.scanPath, primaryFile);
5421
5641
  const filesTouched = [];
5422
5642
  const depsAdded = [];
5423
- const pkgPath = path37.join(ctx.scanPath, "package.json");
5643
+ const pkgPath = path38.join(ctx.scanPath, "package.json");
5424
5644
  const pkg = await readPackageJson(ctx.scanPath);
5425
5645
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5426
5646
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
@@ -5466,7 +5686,7 @@ async function dryRunExtension(ctx, args) {
5466
5686
  };
5467
5687
  }
5468
5688
  for (const file of hookFiles) {
5469
- const content = await fs22.readFile(path37.join(ctx.scanPath, file), "utf8");
5689
+ const content = await fs22.readFile(path38.join(ctx.scanPath, file), "utf8");
5470
5690
  if (content.includes(args.registration_snippet)) {
5471
5691
  return {
5472
5692
  library: args.library,
@@ -5488,7 +5708,7 @@ async function dryRunExtension(ctx, args) {
5488
5708
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
5489
5709
  filesTouched.push("package.json");
5490
5710
  }
5491
- const hookContent = await fs22.readFile(path37.join(ctx.scanPath, primaryFile), "utf8");
5711
+ const hookContent = await fs22.readFile(path38.join(ctx.scanPath, primaryFile), "utf8");
5492
5712
  const patched = splicedContent(hookContent, args.registration_snippet);
5493
5713
  if (patched) {
5494
5714
  filesTouched.push(primaryFile);
@@ -5509,7 +5729,7 @@ async function rollbackExtension(ctx, args) {
5509
5729
  if (!match) {
5510
5730
  return { undone: false, message: "no apply found for library" };
5511
5731
  }
5512
- const pkgPath = path37.join(ctx.scanPath, "package.json");
5732
+ const pkgPath = path38.join(ctx.scanPath, "package.json");
5513
5733
  if (await fileExists2(pkgPath)) {
5514
5734
  const pkg = await readPackageJson(ctx.scanPath);
5515
5735
  if (pkg.dependencies?.[match.instrumentation_package]) {
@@ -5520,7 +5740,7 @@ async function rollbackExtension(ctx, args) {
5520
5740
  }
5521
5741
  const hookFiles = await findHookFiles(ctx.scanPath);
5522
5742
  for (const file of hookFiles) {
5523
- const filePath = path37.join(ctx.scanPath, file);
5743
+ const filePath = path38.join(ctx.scanPath, file);
5524
5744
  const content = await fs22.readFile(filePath, "utf8");
5525
5745
  if (content.includes(match.registration_snippet)) {
5526
5746
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
@@ -5545,6 +5765,7 @@ function handleSse(req, reply, opts) {
5545
5765
  reply.raw.setHeader("Connection", "keep-alive");
5546
5766
  reply.raw.setHeader("X-Accel-Buffering", "no");
5547
5767
  reply.raw.flushHeaders?.();
5768
+ reply.raw.write(":open\n\n");
5548
5769
  let pending = 0;
5549
5770
  let dropped = false;
5550
5771
  const closeConnection = () => {
@@ -6254,6 +6475,7 @@ export {
6254
6475
  setStatus,
6255
6476
  touchLastSeen,
6256
6477
  removeProject,
6478
+ pruneRegistry,
6257
6479
  buildApi
6258
6480
  };
6259
- //# sourceMappingURL=chunk-XS4CGNRO.js.map
6481
+ //# sourceMappingURL=chunk-LUDSPX5N.js.map