@neat.is/core 0.4.11 → 0.4.12

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, path34, edges) {
458
- if (path34.length > best.path.length) {
459
- best = { path: [...path34], edges: [...edges] };
457
+ function step(node, path36, edges) {
458
+ if (path36.length > best.path.length) {
459
+ best = { path: [...path36], edges: [...edges] };
460
460
  }
461
- if (path34.length - 1 >= maxDepth) return;
461
+ if (path36.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
- path34.push(srcId);
466
+ path36.push(srcId);
467
467
  edges.push(edge);
468
- step(srcId, path34, edges);
469
- path34.pop();
468
+ step(srcId, path36, edges);
469
+ path36.pop();
470
470
  edges.pop();
471
471
  visited.delete(srcId);
472
472
  }
@@ -1385,7 +1385,7 @@ function ensureFrontierNode(graph, host, ts) {
1385
1385
  graph.addNode(id, node);
1386
1386
  return id;
1387
1387
  }
1388
- function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1388
+ function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
1389
1389
  if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
1390
1390
  const id = makeObservedEdgeId(type, source, target);
1391
1391
  if (graph.hasEdge(id)) {
@@ -1422,7 +1422,10 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1422
1422
  confidence: confidenceForObservedSignal(signal),
1423
1423
  lastObserved: ts,
1424
1424
  callCount: 1,
1425
- signal
1425
+ signal,
1426
+ // Call-site evidence from span code.* semconv (file-awareness.md §4 + §6).
1427
+ // Only set when code.filepath was present on the span — never fabricated.
1428
+ ...evidence ? { evidence } : {}
1426
1429
  };
1427
1430
  graph.addEdgeWithKey(id, source, target, edge);
1428
1431
  return { edge, created: true };
@@ -1516,6 +1519,7 @@ async function handleSpan(ctx, span) {
1516
1519
  const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
1517
1520
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
1518
1521
  const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
1522
+ const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
1519
1523
  let affectedNode = sourceId;
1520
1524
  const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
1521
1525
  if (span.dbSystem) {
@@ -1529,7 +1533,8 @@ async function handleSpan(ctx, span) {
1529
1533
  observedSource(),
1530
1534
  targetId,
1531
1535
  ts,
1532
- isError
1536
+ isError,
1537
+ callSiteEvidence
1533
1538
  );
1534
1539
  if (result) affectedNode = targetId;
1535
1540
  }
@@ -1545,7 +1550,8 @@ async function handleSpan(ctx, span) {
1545
1550
  observedSource(),
1546
1551
  targetId,
1547
1552
  ts,
1548
- isError
1553
+ isError,
1554
+ callSiteEvidence
1549
1555
  );
1550
1556
  affectedNode = targetId;
1551
1557
  resolvedViaAddress = true;
@@ -1557,7 +1563,8 @@ async function handleSpan(ctx, span) {
1557
1563
  observedSource(),
1558
1564
  frontierNodeId,
1559
1565
  ts,
1560
- isError
1566
+ isError,
1567
+ callSiteEvidence
1561
1568
  );
1562
1569
  affectedNode = frontierNodeId;
1563
1570
  resolvedViaAddress = true;
@@ -2513,19 +2520,122 @@ async function addServiceAliases(graph, scanPath, services) {
2513
2520
  }
2514
2521
 
2515
2522
  // src/extract/databases/index.ts
2516
- import path17 from "path";
2523
+ import path18 from "path";
2524
+ import {
2525
+ EdgeType as EdgeType5,
2526
+ NodeType as NodeType7,
2527
+ Provenance as Provenance3,
2528
+ databaseId as databaseId2,
2529
+ confidenceForExtracted as confidenceForExtracted2
2530
+ } from "@neat.is/types";
2531
+
2532
+ // src/extract/calls/shared.ts
2533
+ import { promises as fs10 } from "fs";
2534
+ import path10 from "path";
2517
2535
  import {
2518
2536
  EdgeType as EdgeType4,
2519
2537
  NodeType as NodeType6,
2520
2538
  Provenance as Provenance2,
2521
- databaseId as databaseId2,
2522
- confidenceForExtracted
2539
+ confidenceForExtracted,
2540
+ extractedEdgeId as extractedEdgeId3,
2541
+ fileId as fileId2
2523
2542
  } from "@neat.is/types";
2543
+ async function walkSourceFiles(dir) {
2544
+ const out = [];
2545
+ async function walk(current) {
2546
+ const entries = await fs10.readdir(current, { withFileTypes: true }).catch(() => []);
2547
+ for (const entry of entries) {
2548
+ const full = path10.join(current, entry.name);
2549
+ if (entry.isDirectory()) {
2550
+ if (IGNORED_DIRS.has(entry.name)) continue;
2551
+ if (await isPythonVenvDir(full)) continue;
2552
+ await walk(full);
2553
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name))) {
2554
+ out.push(full);
2555
+ }
2556
+ }
2557
+ }
2558
+ await walk(dir);
2559
+ return out;
2560
+ }
2561
+ async function loadSourceFiles(dir) {
2562
+ const paths = await walkSourceFiles(dir);
2563
+ const out = [];
2564
+ for (const p of paths) {
2565
+ try {
2566
+ const content = await fs10.readFile(p, "utf8");
2567
+ out.push({ path: p, content });
2568
+ } catch {
2569
+ }
2570
+ }
2571
+ return out;
2572
+ }
2573
+ function lineOf(text, needle) {
2574
+ const idx = text.indexOf(needle);
2575
+ if (idx < 0) return 1;
2576
+ return text.slice(0, idx).split("\n").length;
2577
+ }
2578
+ function snippet(text, line) {
2579
+ const lines = text.split("\n");
2580
+ return (lines[line - 1] ?? "").trim();
2581
+ }
2582
+ function toPosix2(p) {
2583
+ return p.split("\\").join("/");
2584
+ }
2585
+ function languageForPath(relPath) {
2586
+ switch (path10.extname(relPath).toLowerCase()) {
2587
+ case ".py":
2588
+ return "python";
2589
+ case ".ts":
2590
+ case ".tsx":
2591
+ return "typescript";
2592
+ case ".js":
2593
+ case ".jsx":
2594
+ case ".mjs":
2595
+ case ".cjs":
2596
+ return "javascript";
2597
+ default:
2598
+ return void 0;
2599
+ }
2600
+ }
2601
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
2602
+ let nodesAdded = 0;
2603
+ let edgesAdded = 0;
2604
+ const fileNodeId = fileId2(serviceName, relPath);
2605
+ if (!graph.hasNode(fileNodeId)) {
2606
+ const language = languageForPath(relPath);
2607
+ const node = {
2608
+ id: fileNodeId,
2609
+ type: NodeType6.FileNode,
2610
+ service: serviceName,
2611
+ path: relPath,
2612
+ ...language ? { language } : {},
2613
+ discoveredVia: "static"
2614
+ };
2615
+ graph.addNode(fileNodeId, node);
2616
+ nodesAdded++;
2617
+ }
2618
+ const containsId = extractedEdgeId3(serviceNodeId, fileNodeId, EdgeType4.CONTAINS);
2619
+ if (!graph.hasEdge(containsId)) {
2620
+ const edge = {
2621
+ id: containsId,
2622
+ source: serviceNodeId,
2623
+ target: fileNodeId,
2624
+ type: EdgeType4.CONTAINS,
2625
+ provenance: Provenance2.EXTRACTED,
2626
+ confidence: confidenceForExtracted("structural"),
2627
+ evidence: { file: relPath }
2628
+ };
2629
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
2630
+ edgesAdded++;
2631
+ }
2632
+ return { fileNodeId, nodesAdded, edgesAdded };
2633
+ }
2524
2634
 
2525
2635
  // src/extract/databases/db-config-yaml.ts
2526
- import path10 from "path";
2636
+ import path11 from "path";
2527
2637
  async function parse(serviceDir) {
2528
- const yamlPath = path10.join(serviceDir, "db-config.yaml");
2638
+ const yamlPath = path11.join(serviceDir, "db-config.yaml");
2529
2639
  if (!await exists(yamlPath)) return [];
2530
2640
  const raw = await readYaml(yamlPath);
2531
2641
  return [
@@ -2542,12 +2652,12 @@ async function parse(serviceDir) {
2542
2652
  var dbConfigYamlParser = { name: "db-config.yaml", parse };
2543
2653
 
2544
2654
  // src/extract/databases/dotenv.ts
2545
- import { promises as fs11 } from "fs";
2546
- import path12 from "path";
2655
+ import { promises as fs12 } from "fs";
2656
+ import path13 from "path";
2547
2657
 
2548
2658
  // src/extract/databases/shared.ts
2549
- import { promises as fs10 } from "fs";
2550
- import path11 from "path";
2659
+ import { promises as fs11 } from "fs";
2660
+ import path12 from "path";
2551
2661
  function schemeToEngine(scheme) {
2552
2662
  const s = scheme.toLowerCase().split("+")[0];
2553
2663
  switch (s) {
@@ -2586,14 +2696,14 @@ function parseConnectionString(url) {
2586
2696
  }
2587
2697
  async function readIfExists(filePath) {
2588
2698
  try {
2589
- return await fs10.readFile(filePath, "utf8");
2699
+ return await fs11.readFile(filePath, "utf8");
2590
2700
  } catch {
2591
2701
  return null;
2592
2702
  }
2593
2703
  }
2594
2704
  async function findFirst(serviceDir, candidates) {
2595
2705
  for (const rel of candidates) {
2596
- const abs = path11.join(serviceDir, rel);
2706
+ const abs = path12.join(serviceDir, rel);
2597
2707
  const content = await readIfExists(abs);
2598
2708
  if (content !== null) return abs;
2599
2709
  }
@@ -2644,15 +2754,15 @@ function parseDotenvLine(line) {
2644
2754
  return { key, value };
2645
2755
  }
2646
2756
  async function parse2(serviceDir) {
2647
- const entries = await fs11.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2757
+ const entries = await fs12.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2648
2758
  const configs = [];
2649
2759
  const seen = /* @__PURE__ */ new Set();
2650
2760
  for (const entry of entries) {
2651
2761
  if (!entry.isFile()) continue;
2652
2762
  const match = isConfigFile(entry.name);
2653
2763
  if (!match.match || match.fileType !== "env") continue;
2654
- const filePath = path12.join(serviceDir, entry.name);
2655
- const content = await fs11.readFile(filePath, "utf8");
2764
+ const filePath = path13.join(serviceDir, entry.name);
2765
+ const content = await fs12.readFile(filePath, "utf8");
2656
2766
  for (const line of content.split("\n")) {
2657
2767
  const parsed = parseDotenvLine(line);
2658
2768
  if (!parsed) continue;
@@ -2670,9 +2780,9 @@ async function parse2(serviceDir) {
2670
2780
  var dotenvParser = { name: ".env", parse: parse2 };
2671
2781
 
2672
2782
  // src/extract/databases/prisma.ts
2673
- import path13 from "path";
2783
+ import path14 from "path";
2674
2784
  async function parse3(serviceDir) {
2675
- const schemaPath = path13.join(serviceDir, "prisma", "schema.prisma");
2785
+ const schemaPath = path14.join(serviceDir, "prisma", "schema.prisma");
2676
2786
  const content = await readIfExists(schemaPath);
2677
2787
  if (!content) return [];
2678
2788
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -2801,10 +2911,10 @@ async function parse5(serviceDir) {
2801
2911
  var knexParser = { name: "knex", parse: parse5 };
2802
2912
 
2803
2913
  // src/extract/databases/ormconfig.ts
2804
- import path14 from "path";
2914
+ import path15 from "path";
2805
2915
  async function parse6(serviceDir) {
2806
2916
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
2807
- const abs = path14.join(serviceDir, candidate);
2917
+ const abs = path15.join(serviceDir, candidate);
2808
2918
  if (!await exists(abs)) continue;
2809
2919
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
2810
2920
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -2862,9 +2972,9 @@ async function parse7(serviceDir) {
2862
2972
  var typeormParser = { name: "typeorm", parse: parse7 };
2863
2973
 
2864
2974
  // src/extract/databases/sequelize.ts
2865
- import path15 from "path";
2975
+ import path16 from "path";
2866
2976
  async function parse8(serviceDir) {
2867
- const configPath = path15.join(serviceDir, "config", "config.json");
2977
+ const configPath = path16.join(serviceDir, "config", "config.json");
2868
2978
  if (!await exists(configPath)) return [];
2869
2979
  const raw = await readJson(configPath);
2870
2980
  const out = [];
@@ -2890,7 +3000,7 @@ async function parse8(serviceDir) {
2890
3000
  var sequelizeParser = { name: "sequelize", parse: parse8 };
2891
3001
 
2892
3002
  // src/extract/databases/docker-compose.ts
2893
- import path16 from "path";
3003
+ import path17 from "path";
2894
3004
  function portFromService(svc) {
2895
3005
  for (const raw of svc.ports ?? []) {
2896
3006
  const str = String(raw);
@@ -2917,7 +3027,7 @@ function databaseFromEnv(svc) {
2917
3027
  }
2918
3028
  async function parse9(serviceDir) {
2919
3029
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2920
- const abs = path16.join(serviceDir, name);
3030
+ const abs = path17.join(serviceDir, name);
2921
3031
  if (!await exists(abs)) continue;
2922
3032
  const raw = await readYaml(abs);
2923
3033
  if (!raw?.services) return [];
@@ -2959,7 +3069,7 @@ function compatibleDriversFor(engine) {
2959
3069
  function toDatabaseNode(config) {
2960
3070
  return {
2961
3071
  id: databaseId2(config.host),
2962
- type: NodeType6.DatabaseNode,
3072
+ type: NodeType7.DatabaseNode,
2963
3073
  name: config.database || config.host,
2964
3074
  engine: config.engine,
2965
3075
  engineVersion: config.engineVersion,
@@ -3089,19 +3199,24 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3089
3199
  discoveredVia: mergedDiscoveredVia
3090
3200
  });
3091
3201
  }
3202
+ const relConfigFile = toPosix2(path18.relative(service.dir, config.sourceFile));
3203
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
3204
+ graph,
3205
+ service.pkg.name,
3206
+ service.node.id,
3207
+ relConfigFile
3208
+ );
3209
+ nodesAdded += fn;
3210
+ edgesAdded += fe;
3211
+ const evidenceFile = toPosix2(path18.relative(scanPath, config.sourceFile));
3092
3212
  const edge = {
3093
- id: extractedEdgeId2(service.node.id, dbNode.id, EdgeType4.CONNECTS_TO),
3094
- source: service.node.id,
3213
+ id: extractedEdgeId2(fileNodeId, dbNode.id, EdgeType5.CONNECTS_TO),
3214
+ source: fileNodeId,
3095
3215
  target: dbNode.id,
3096
- type: EdgeType4.CONNECTS_TO,
3097
- provenance: Provenance2.EXTRACTED,
3098
- confidence: confidenceForExtracted("structural"),
3099
- // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3100
- // Ghost-edge cleanup keys retirement on this; the conditional
3101
- // sourceFile spread that used to live here was a v0.1.x leftover.
3102
- evidence: {
3103
- file: path17.relative(scanPath, config.sourceFile).split(path17.sep).join("/")
3104
- }
3216
+ type: EdgeType5.CONNECTS_TO,
3217
+ provenance: Provenance3.EXTRACTED,
3218
+ confidence: confidenceForExtracted2("structural"),
3219
+ evidence: { file: evidenceFile }
3105
3220
  };
3106
3221
  if (!graph.hasEdge(edge.id)) {
3107
3222
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3126,21 +3241,21 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3126
3241
  }
3127
3242
 
3128
3243
  // src/extract/configs.ts
3129
- import { promises as fs12 } from "fs";
3130
- import path18 from "path";
3244
+ import { promises as fs13 } from "fs";
3245
+ import path19 from "path";
3131
3246
  import {
3132
- EdgeType as EdgeType5,
3133
- NodeType as NodeType7,
3134
- Provenance as Provenance3,
3247
+ EdgeType as EdgeType6,
3248
+ NodeType as NodeType8,
3249
+ Provenance as Provenance4,
3135
3250
  configId,
3136
- confidenceForExtracted as confidenceForExtracted2
3251
+ confidenceForExtracted as confidenceForExtracted3
3137
3252
  } from "@neat.is/types";
3138
3253
  async function walkConfigFiles(dir) {
3139
3254
  const out = [];
3140
3255
  async function walk(current) {
3141
- const entries = await fs12.readdir(current, { withFileTypes: true });
3256
+ const entries = await fs13.readdir(current, { withFileTypes: true });
3142
3257
  for (const entry of entries) {
3143
- const full = path18.join(current, entry.name);
3258
+ const full = path19.join(current, entry.name);
3144
3259
  if (entry.isDirectory()) {
3145
3260
  if (IGNORED_DIRS.has(entry.name)) continue;
3146
3261
  if (await isPythonVenvDir(full)) continue;
@@ -3159,26 +3274,35 @@ async function addConfigNodes(graph, services, scanPath) {
3159
3274
  for (const service of services) {
3160
3275
  const configFiles = await walkConfigFiles(service.dir);
3161
3276
  for (const file of configFiles) {
3162
- const relPath = path18.relative(scanPath, file);
3277
+ const relPath = path19.relative(scanPath, file);
3163
3278
  const node = {
3164
3279
  id: configId(relPath),
3165
- type: NodeType7.ConfigNode,
3166
- name: path18.basename(file),
3280
+ type: NodeType8.ConfigNode,
3281
+ name: path19.basename(file),
3167
3282
  path: relPath,
3168
- fileType: isConfigFile(path18.basename(file)).fileType
3283
+ fileType: isConfigFile(path19.basename(file)).fileType
3169
3284
  };
3170
3285
  if (!graph.hasNode(node.id)) {
3171
3286
  graph.addNode(node.id, node);
3172
3287
  nodesAdded++;
3173
3288
  }
3289
+ const relToService = toPosix2(path19.relative(service.dir, file));
3290
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
3291
+ graph,
3292
+ service.pkg.name,
3293
+ service.node.id,
3294
+ relToService
3295
+ );
3296
+ nodesAdded += fn;
3297
+ edgesAdded += fe;
3174
3298
  const edge = {
3175
- id: extractedEdgeId2(service.node.id, node.id, EdgeType5.CONFIGURED_BY),
3176
- source: service.node.id,
3299
+ id: extractedEdgeId2(fileNodeId, node.id, EdgeType6.CONFIGURED_BY),
3300
+ source: fileNodeId,
3177
3301
  target: node.id,
3178
- type: EdgeType5.CONFIGURED_BY,
3179
- provenance: Provenance3.EXTRACTED,
3180
- confidence: confidenceForExtracted2("structural"),
3181
- evidence: { file: relPath.split(path18.sep).join("/") }
3302
+ type: EdgeType6.CONFIGURED_BY,
3303
+ provenance: Provenance4.EXTRACTED,
3304
+ confidence: confidenceForExtracted3("structural"),
3305
+ evidence: { file: relPath.split(path19.sep).join("/") }
3182
3306
  };
3183
3307
  if (!graph.hasEdge(edge.id)) {
3184
3308
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3209,111 +3333,6 @@ import {
3209
3333
  confidenceForExtracted as confidenceForExtracted4,
3210
3334
  passesExtractedFloor
3211
3335
  } from "@neat.is/types";
3212
-
3213
- // src/extract/calls/shared.ts
3214
- import { promises as fs13 } from "fs";
3215
- import path19 from "path";
3216
- import {
3217
- EdgeType as EdgeType6,
3218
- NodeType as NodeType8,
3219
- Provenance as Provenance4,
3220
- confidenceForExtracted as confidenceForExtracted3,
3221
- extractedEdgeId as extractedEdgeId3,
3222
- fileId as fileId2
3223
- } from "@neat.is/types";
3224
- async function walkSourceFiles(dir) {
3225
- const out = [];
3226
- async function walk(current) {
3227
- const entries = await fs13.readdir(current, { withFileTypes: true }).catch(() => []);
3228
- for (const entry of entries) {
3229
- const full = path19.join(current, entry.name);
3230
- if (entry.isDirectory()) {
3231
- if (IGNORED_DIRS.has(entry.name)) continue;
3232
- if (await isPythonVenvDir(full)) continue;
3233
- await walk(full);
3234
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path19.extname(entry.name))) {
3235
- out.push(full);
3236
- }
3237
- }
3238
- }
3239
- await walk(dir);
3240
- return out;
3241
- }
3242
- async function loadSourceFiles(dir) {
3243
- const paths = await walkSourceFiles(dir);
3244
- const out = [];
3245
- for (const p of paths) {
3246
- try {
3247
- const content = await fs13.readFile(p, "utf8");
3248
- out.push({ path: p, content });
3249
- } catch {
3250
- }
3251
- }
3252
- return out;
3253
- }
3254
- function lineOf(text, needle) {
3255
- const idx = text.indexOf(needle);
3256
- if (idx < 0) return 1;
3257
- return text.slice(0, idx).split("\n").length;
3258
- }
3259
- function snippet(text, line) {
3260
- const lines = text.split("\n");
3261
- return (lines[line - 1] ?? "").trim();
3262
- }
3263
- function toPosix2(p) {
3264
- return p.split("\\").join("/");
3265
- }
3266
- function languageForPath(relPath) {
3267
- switch (path19.extname(relPath).toLowerCase()) {
3268
- case ".py":
3269
- return "python";
3270
- case ".ts":
3271
- case ".tsx":
3272
- return "typescript";
3273
- case ".js":
3274
- case ".jsx":
3275
- case ".mjs":
3276
- case ".cjs":
3277
- return "javascript";
3278
- default:
3279
- return void 0;
3280
- }
3281
- }
3282
- function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3283
- let nodesAdded = 0;
3284
- let edgesAdded = 0;
3285
- const fileNodeId = fileId2(serviceName, relPath);
3286
- if (!graph.hasNode(fileNodeId)) {
3287
- const language = languageForPath(relPath);
3288
- const node = {
3289
- id: fileNodeId,
3290
- type: NodeType8.FileNode,
3291
- service: serviceName,
3292
- path: relPath,
3293
- ...language ? { language } : {},
3294
- discoveredVia: "static"
3295
- };
3296
- graph.addNode(fileNodeId, node);
3297
- nodesAdded++;
3298
- }
3299
- const containsId = extractedEdgeId3(serviceNodeId, fileNodeId, EdgeType6.CONTAINS);
3300
- if (!graph.hasEdge(containsId)) {
3301
- const edge = {
3302
- id: containsId,
3303
- source: serviceNodeId,
3304
- target: fileNodeId,
3305
- type: EdgeType6.CONTAINS,
3306
- provenance: Provenance4.EXTRACTED,
3307
- confidence: confidenceForExtracted3("structural"),
3308
- evidence: { file: relPath }
3309
- };
3310
- graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3311
- edgesAdded++;
3312
- }
3313
- return { fileNodeId, nodesAdded, edgesAdded };
3314
- }
3315
-
3316
- // src/extract/calls/http.ts
3317
3336
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3318
3337
  var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3319
3338
  function isInsideJsxExternalLink(node) {
@@ -3593,6 +3612,7 @@ var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-
3593
3612
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
3594
3613
  function isLikelyAddress(value) {
3595
3614
  if (!value) return false;
3615
+ if (/\s/.test(value) || value.startsWith("{")) return false;
3596
3616
  return /:\d{2,5}$/.test(value) || value.includes(".");
3597
3617
  }
3598
3618
  function normaliseForMatch(s) {
@@ -3898,17 +3918,26 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3898
3918
  graph.addNode(node.id, node);
3899
3919
  nodesAdded++;
3900
3920
  }
3901
- const edgeId = extractedEdgeId2(service.node.id, node.id, EdgeType10.RUNS_ON);
3921
+ const relDockerfile = toPosix2(path26.relative(service.dir, dockerfilePath));
3922
+ const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
3923
+ graph,
3924
+ service.pkg.name,
3925
+ service.node.id,
3926
+ relDockerfile
3927
+ );
3928
+ nodesAdded += fn;
3929
+ edgesAdded += fe;
3930
+ const edgeId = extractedEdgeId2(fileNodeId, node.id, EdgeType10.RUNS_ON);
3902
3931
  if (!graph.hasEdge(edgeId)) {
3903
3932
  const edge = {
3904
3933
  id: edgeId,
3905
- source: service.node.id,
3934
+ source: fileNodeId,
3906
3935
  target: node.id,
3907
3936
  type: EdgeType10.RUNS_ON,
3908
3937
  provenance: Provenance8.EXTRACTED,
3909
3938
  confidence: confidenceForExtracted7("structural"),
3910
3939
  evidence: {
3911
- file: path26.relative(scanPath, dockerfilePath).split(path26.sep).join("/")
3940
+ file: toPosix2(path26.relative(scanPath, dockerfilePath))
3912
3941
  }
3913
3942
  };
3914
3943
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4868,6 +4897,319 @@ import Fastify from "fastify";
4868
4897
  import cors from "@fastify/cors";
4869
4898
  import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } from "@neat.is/types";
4870
4899
 
4900
+ // src/extend/index.ts
4901
+ import { promises as fs21 } from "fs";
4902
+ import path35 from "path";
4903
+ import os3 from "os";
4904
+ import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
4905
+
4906
+ // src/installers/package-manager.ts
4907
+ import { promises as fs20 } from "fs";
4908
+ import path34 from "path";
4909
+ import { spawn } from "child_process";
4910
+ var LOCKFILE_PRIORITY = [
4911
+ { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
4912
+ { lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
4913
+ { lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
4914
+ {
4915
+ lockfile: "package-lock.json",
4916
+ pm: "npm",
4917
+ args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
4918
+ }
4919
+ ];
4920
+ var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
4921
+ async function exists2(p) {
4922
+ try {
4923
+ await fs20.access(p);
4924
+ return true;
4925
+ } catch {
4926
+ return false;
4927
+ }
4928
+ }
4929
+ async function detectPackageManager(serviceDir) {
4930
+ let dir = path34.resolve(serviceDir);
4931
+ const stops = /* @__PURE__ */ new Set();
4932
+ for (let i = 0; i < 64; i++) {
4933
+ if (stops.has(dir)) break;
4934
+ stops.add(dir);
4935
+ for (const candidate of LOCKFILE_PRIORITY) {
4936
+ const lockPath = path34.join(dir, candidate.lockfile);
4937
+ if (await exists2(lockPath)) {
4938
+ return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
4939
+ }
4940
+ }
4941
+ const parent = path34.dirname(dir);
4942
+ if (parent === dir) break;
4943
+ dir = parent;
4944
+ }
4945
+ return { pm: "npm", cwd: path34.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
4946
+ }
4947
+ async function runPackageManagerInstall(cmd) {
4948
+ return new Promise((resolve) => {
4949
+ const child = spawn(cmd.pm, cmd.args, {
4950
+ cwd: cmd.cwd,
4951
+ // Inherit PATH + HOME so the user's installed managers resolve.
4952
+ env: process.env,
4953
+ // `false` keeps the parent in control of cleanup if the orchestrator
4954
+ // exits before install finishes. Cross-platform-safe.
4955
+ shell: false,
4956
+ stdio: ["ignore", "ignore", "pipe"]
4957
+ });
4958
+ let stderr = "";
4959
+ child.stderr?.on("data", (chunk) => {
4960
+ stderr += chunk.toString("utf8");
4961
+ });
4962
+ child.on("error", (err) => {
4963
+ resolve({
4964
+ pm: cmd.pm,
4965
+ cwd: cmd.cwd,
4966
+ args: cmd.args,
4967
+ exitCode: 127,
4968
+ stderr: stderr + `
4969
+ ${err.message}`
4970
+ });
4971
+ });
4972
+ child.on("close", (code) => {
4973
+ resolve({
4974
+ pm: cmd.pm,
4975
+ cwd: cmd.cwd,
4976
+ args: cmd.args,
4977
+ exitCode: code ?? 1,
4978
+ stderr: stderr.trim()
4979
+ });
4980
+ });
4981
+ });
4982
+ }
4983
+
4984
+ // src/extend/index.ts
4985
+ async function fileExists(p) {
4986
+ try {
4987
+ await fs21.access(p);
4988
+ return true;
4989
+ } catch {
4990
+ return false;
4991
+ }
4992
+ }
4993
+ async function readPackageJson(scanPath) {
4994
+ const pkgPath = path35.join(scanPath, "package.json");
4995
+ const raw = await fs21.readFile(pkgPath, "utf8");
4996
+ return JSON.parse(raw);
4997
+ }
4998
+ async function findHookFiles(scanPath) {
4999
+ const entries = await fs21.readdir(scanPath);
5000
+ return entries.filter(
5001
+ (e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
5002
+ ).sort();
5003
+ }
5004
+ function extendLogPath() {
5005
+ return process.env.NEAT_EXTEND_LOG ?? path35.join(os3.homedir(), ".neat", "extend-log.ndjson");
5006
+ }
5007
+ async function appendExtendLog(entry) {
5008
+ const logPath = extendLogPath();
5009
+ await fs21.mkdir(path35.dirname(logPath), { recursive: true });
5010
+ await fs21.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
5011
+ }
5012
+ function splicedContent(fileContent, snippet2) {
5013
+ if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
5014
+ return fileContent.replace("__INSTRUMENTATION_BLOCK__", `${snippet2}
5015
+ __INSTRUMENTATION_BLOCK__`);
5016
+ }
5017
+ const lines = fileContent.split("\n");
5018
+ let lastPushIdx = -1;
5019
+ for (let i = 0; i < lines.length; i++) {
5020
+ if (lines[i].includes("instrumentations.push(")) lastPushIdx = i;
5021
+ }
5022
+ if (lastPushIdx >= 0) {
5023
+ lines.splice(lastPushIdx + 1, 0, snippet2);
5024
+ return lines.join("\n");
5025
+ }
5026
+ for (let i = 0; i < lines.length; i++) {
5027
+ if (lines[i].includes("new NodeSDK(")) {
5028
+ lines.splice(i, 0, snippet2);
5029
+ return lines.join("\n");
5030
+ }
5031
+ }
5032
+ return null;
5033
+ }
5034
+ async function listUninstrumented(ctx) {
5035
+ const pkg = await readPackageJson(ctx.scanPath);
5036
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5037
+ const results = [];
5038
+ for (const [library, installedVersion] of Object.entries(allDeps)) {
5039
+ const entry = registryResolve(library, installedVersion);
5040
+ if (!entry) continue;
5041
+ if (entry.coverage === "bundled" || entry.coverage === "http-only") continue;
5042
+ results.push({
5043
+ library,
5044
+ coverage: entry.coverage,
5045
+ installedVersion,
5046
+ instrumentation_package: entry.instrumentation_package,
5047
+ package_version: entry.package_version,
5048
+ registration: entry.registration,
5049
+ notes: entry.notes
5050
+ });
5051
+ }
5052
+ return results;
5053
+ }
5054
+ function lookupInstrumentation(library, installedVersion) {
5055
+ const entry = registryResolve(library, installedVersion);
5056
+ if (!entry) return null;
5057
+ return {
5058
+ library: entry.library,
5059
+ coverage: entry.coverage,
5060
+ instrumentation_package: entry.instrumentation_package,
5061
+ package_version: entry.package_version,
5062
+ registration: entry.registration,
5063
+ notes: entry.notes
5064
+ };
5065
+ }
5066
+ async function describeProjectInstrumentation(ctx) {
5067
+ const hookFiles = await findHookFiles(ctx.scanPath);
5068
+ const envNeat = await fileExists(path35.join(ctx.scanPath, ".env.neat"));
5069
+ const registryInstrPackages = new Set(
5070
+ registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
5071
+ );
5072
+ const pkg = await readPackageJson(ctx.scanPath);
5073
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
5074
+ const installedDeps = {};
5075
+ for (const [key, version] of Object.entries(allDeps)) {
5076
+ if (key.startsWith("@opentelemetry/") || registryInstrPackages.has(key)) {
5077
+ installedDeps[key] = version;
5078
+ }
5079
+ }
5080
+ return { hookFiles, envNeat, installedDeps };
5081
+ }
5082
+ async function applyExtension(ctx, args, options) {
5083
+ const hookFiles = await findHookFiles(ctx.scanPath);
5084
+ if (hookFiles.length === 0) {
5085
+ throw new Error(
5086
+ `No instrumentation hook files found in ${ctx.scanPath}. Run \`neat init\` first.`
5087
+ );
5088
+ }
5089
+ for (const file of hookFiles) {
5090
+ const content = await fs21.readFile(path35.join(ctx.scanPath, file), "utf8");
5091
+ if (content.includes(args.registration_snippet)) {
5092
+ return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
5093
+ }
5094
+ }
5095
+ const primaryFile = hookFiles[0];
5096
+ const primaryPath = path35.join(ctx.scanPath, primaryFile);
5097
+ const filesTouched = [];
5098
+ const depsAdded = [];
5099
+ const pkgPath = path35.join(ctx.scanPath, "package.json");
5100
+ const pkg = await readPackageJson(ctx.scanPath);
5101
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5102
+ pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
5103
+ await fs21.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
5104
+ filesTouched.push("package.json");
5105
+ depsAdded.push(`${args.instrumentation_package}@${args.version}`);
5106
+ }
5107
+ const hookContent = await fs21.readFile(primaryPath, "utf8");
5108
+ const patched = splicedContent(hookContent, args.registration_snippet);
5109
+ if (!patched) {
5110
+ throw new Error(
5111
+ `Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
5112
+ );
5113
+ }
5114
+ await fs21.writeFile(primaryPath, patched, "utf8");
5115
+ filesTouched.push(primaryFile);
5116
+ const cmd = await detectPackageManager(ctx.scanPath);
5117
+ const installer = options?.runInstall ?? runPackageManagerInstall;
5118
+ const install = await installer(cmd);
5119
+ const installOutput = install.exitCode === 0 ? `${cmd.pm} install succeeded` : install.stderr || `${cmd.pm} install failed (exit ${install.exitCode})`;
5120
+ await appendExtendLog({
5121
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5122
+ project: ctx.project,
5123
+ library: args.library,
5124
+ instrumentation_package: args.instrumentation_package,
5125
+ version: args.version,
5126
+ registration_snippet: args.registration_snippet,
5127
+ filesTouched,
5128
+ depsAdded,
5129
+ installOutput
5130
+ });
5131
+ return { library: args.library, filesTouched, depsAdded, installOutput, alreadyApplied: false };
5132
+ }
5133
+ async function dryRunExtension(ctx, args) {
5134
+ const hookFiles = await findHookFiles(ctx.scanPath);
5135
+ if (hookFiles.length === 0) {
5136
+ return {
5137
+ library: args.library,
5138
+ filesTouched: [],
5139
+ depsToAdd: [],
5140
+ packageJsonPatch: {},
5141
+ templatePatch: "No hook files found. Run 'neat init' first."
5142
+ };
5143
+ }
5144
+ for (const file of hookFiles) {
5145
+ const content = await fs21.readFile(path35.join(ctx.scanPath, file), "utf8");
5146
+ if (content.includes(args.registration_snippet)) {
5147
+ return {
5148
+ library: args.library,
5149
+ filesTouched: [],
5150
+ depsToAdd: [],
5151
+ packageJsonPatch: {},
5152
+ templatePatch: "Already applied \u2014 no changes would be made."
5153
+ };
5154
+ }
5155
+ }
5156
+ const primaryFile = hookFiles[0];
5157
+ const filesTouched = [];
5158
+ const depsToAdd = [];
5159
+ let packageJsonPatch = {};
5160
+ let templatePatch = "";
5161
+ const pkg = await readPackageJson(ctx.scanPath);
5162
+ if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5163
+ packageJsonPatch = { dependencies: { [args.instrumentation_package]: args.version } };
5164
+ depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
5165
+ filesTouched.push("package.json");
5166
+ }
5167
+ const hookContent = await fs21.readFile(path35.join(ctx.scanPath, primaryFile), "utf8");
5168
+ const patched = splicedContent(hookContent, args.registration_snippet);
5169
+ if (patched) {
5170
+ filesTouched.push(primaryFile);
5171
+ templatePatch = `+ ${args.registration_snippet}`;
5172
+ } else {
5173
+ templatePatch = "Could not find insertion point in hook file.";
5174
+ }
5175
+ return { library: args.library, filesTouched, depsToAdd, packageJsonPatch, templatePatch };
5176
+ }
5177
+ async function rollbackExtension(ctx, args) {
5178
+ const logPath = extendLogPath();
5179
+ if (!await fileExists(logPath)) {
5180
+ return { undone: false, message: "no apply found for library" };
5181
+ }
5182
+ const raw = await fs21.readFile(logPath, "utf8");
5183
+ const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
5184
+ const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
5185
+ if (!match) {
5186
+ return { undone: false, message: "no apply found for library" };
5187
+ }
5188
+ const pkgPath = path35.join(ctx.scanPath, "package.json");
5189
+ if (await fileExists(pkgPath)) {
5190
+ const pkg = await readPackageJson(ctx.scanPath);
5191
+ if (pkg.dependencies?.[match.instrumentation_package]) {
5192
+ const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
5193
+ pkg.dependencies = rest;
5194
+ await fs21.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
5195
+ }
5196
+ }
5197
+ const hookFiles = await findHookFiles(ctx.scanPath);
5198
+ for (const file of hookFiles) {
5199
+ const filePath = path35.join(ctx.scanPath, file);
5200
+ const content = await fs21.readFile(filePath, "utf8");
5201
+ if (content.includes(match.registration_snippet)) {
5202
+ const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
5203
+ await fs21.writeFile(filePath, filtered, "utf8");
5204
+ break;
5205
+ }
5206
+ }
5207
+ return {
5208
+ undone: true,
5209
+ message: `rolled back ${match.library} (${match.instrumentation_package})`
5210
+ };
5211
+ }
5212
+
4871
5213
  // src/streaming.ts
4872
5214
  var SSE_HEARTBEAT_MS = 3e4;
4873
5215
  var SSE_BACKPRESSURE_CAP = 1e3;
@@ -5324,6 +5666,105 @@ function registerRoutes(scope, ctx) {
5324
5666
  violations
5325
5667
  };
5326
5668
  });
5669
+ scope.get("/extend/list-uninstrumented", async (req, reply) => {
5670
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5671
+ if (!proj) return;
5672
+ if (!proj.scanPath) {
5673
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
5674
+ }
5675
+ try {
5676
+ const results = await listUninstrumented({ project: proj.name, scanPath: proj.scanPath });
5677
+ return { libraries: results };
5678
+ } catch (err) {
5679
+ return reply.code(500).send({ error: err.message });
5680
+ }
5681
+ });
5682
+ scope.get("/extend/lookup", async (req, reply) => {
5683
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5684
+ if (!proj) return;
5685
+ const { library, version } = req.query;
5686
+ if (!library) {
5687
+ return reply.code(400).send({ error: "query parameter `library` is required" });
5688
+ }
5689
+ const result = lookupInstrumentation(library, version);
5690
+ if (!result) {
5691
+ return reply.code(404).send({ error: "library not found in registry", library });
5692
+ }
5693
+ return result;
5694
+ });
5695
+ scope.get("/extend/describe", async (req, reply) => {
5696
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5697
+ if (!proj) return;
5698
+ if (!proj.scanPath) {
5699
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
5700
+ }
5701
+ try {
5702
+ const state = await describeProjectInstrumentation({ project: proj.name, scanPath: proj.scanPath });
5703
+ return state;
5704
+ } catch (err) {
5705
+ return reply.code(500).send({ error: err.message });
5706
+ }
5707
+ });
5708
+ scope.post("/extend/apply", async (req, reply) => {
5709
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5710
+ if (!proj) return;
5711
+ if (!proj.scanPath) {
5712
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
5713
+ }
5714
+ const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
5715
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
5716
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
5717
+ }
5718
+ try {
5719
+ const result = await applyExtension(
5720
+ { project: proj.name, scanPath: proj.scanPath },
5721
+ { library, instrumentation_package, version, registration_snippet }
5722
+ );
5723
+ return result;
5724
+ } catch (err) {
5725
+ return reply.code(500).send({ error: err.message });
5726
+ }
5727
+ });
5728
+ scope.post("/extend/dry-run", async (req, reply) => {
5729
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5730
+ if (!proj) return;
5731
+ if (!proj.scanPath) {
5732
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
5733
+ }
5734
+ const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
5735
+ if (!library || !instrumentation_package || !version || !registration_snippet) {
5736
+ return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
5737
+ }
5738
+ try {
5739
+ const result = await dryRunExtension(
5740
+ { project: proj.name, scanPath: proj.scanPath },
5741
+ { library, instrumentation_package, version, registration_snippet }
5742
+ );
5743
+ return result;
5744
+ } catch (err) {
5745
+ return reply.code(500).send({ error: err.message });
5746
+ }
5747
+ });
5748
+ scope.post("/extend/rollback", async (req, reply) => {
5749
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5750
+ if (!proj) return;
5751
+ if (!proj.scanPath) {
5752
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
5753
+ }
5754
+ const { library } = req.body ?? {};
5755
+ if (!library) {
5756
+ return reply.code(400).send({ error: "body must include library" });
5757
+ }
5758
+ try {
5759
+ const result = await rollbackExtension(
5760
+ { project: proj.name, scanPath: proj.scanPath },
5761
+ { library }
5762
+ );
5763
+ return result;
5764
+ } catch (err) {
5765
+ return reply.code(500).send({ error: err.message });
5766
+ }
5767
+ });
5327
5768
  }
5328
5769
  async function buildApi(opts) {
5329
5770
  const app = Fastify({ logger: false });
@@ -5469,6 +5910,8 @@ export {
5469
5910
  saveGraphToDisk,
5470
5911
  loadGraphFromDisk,
5471
5912
  startPersistLoop,
5913
+ detectPackageManager,
5914
+ runPackageManagerInstall,
5472
5915
  loadSnapshotForDiff,
5473
5916
  computeGraphDiff,
5474
5917
  pathsForProject,
@@ -5488,4 +5931,4 @@ export {
5488
5931
  removeProject,
5489
5932
  buildApi
5490
5933
  };
5491
- //# sourceMappingURL=chunk-OJHI33LD.js.map
5934
+ //# sourceMappingURL=chunk-WDG4QEFO.js.map