@g3nomaly/analyze 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -967,7 +967,13 @@ function endsWith(owners, qualifier) {
967
967
  const offset = owners.length - qualifier.length;
968
968
  return qualifier.every((name, index) => owners[offset + index] === name);
969
969
  }
970
- function pick(declarations, scope, qualifier = []) {
970
+ function pick(declarations, scope, qualifier = [], accepts = () => true) {
971
+ const winner = choose(declarations, scope, qualifier);
972
+ if (winner === null || !accepts(winner) || scope.admits !== undefined && !scope.admits(winner))
973
+ return null;
974
+ return winner.ref;
975
+ }
976
+ function choose(declarations, scope, qualifier) {
971
977
  const { path } = scope;
972
978
  const visible = declarations.filter((declaration) => (scope.family === null || declaration.family === scope.family) && !(declaration.local && path !== null && declaration.path !== path) && (scope.sees === undefined || declaration.path === path || scope.sees(declaration.path)) && endsWith(declaration.owners, qualifier));
973
979
  const shared = new Set(visible.flatMap((declaration) => declaration.privateFile ? [] : [declaration.owners.join("\x00")]));
@@ -977,14 +983,14 @@ function pick(declarations, scope, qualifier = []) {
977
983
  return null;
978
984
  const [sameFile, otherInFile] = path === null ? [] : candidates.filter((declaration) => declaration.path === path);
979
985
  if (sameFile)
980
- return otherInFile ? null : sameFile.ref;
986
+ return otherInFile ? null : sameFile;
981
987
  if (!second)
982
- return first.ref;
988
+ return first;
983
989
  if (path === null)
984
990
  return null;
985
991
  const ranked = candidates.map((declaration) => ({ declaration, shared: sharedDirectories(declaration.path, path) })).sort((a, b) => b.shared - a.shared);
986
992
  const [best, runnerUp] = ranked;
987
- return best && runnerUp && best.shared > runnerUp.shared ? best.declaration.ref : null;
993
+ return best && runnerUp && best.shared > runnerUp.shared ? best.declaration : null;
988
994
  }
989
995
 
990
996
  class TypeIndex {
@@ -1020,7 +1026,8 @@ class CallableIndex {
1020
1026
  if (!CALLABLE_NAME.test(text))
1021
1027
  return null;
1022
1028
  const key = text.replace(/\(\)$/, "").replace(/::|#/g, ".");
1023
- return pick(this.byName.get(key) ?? [], scope);
1029
+ const bare = !key.includes(".");
1030
+ return pick(this.byName.get(key) ?? [], scope, [], (site) => !bare || !site.member || site.path === scope.path);
1024
1031
  }
1025
1032
  resolveCall(name, scope, owner, qualifier = []) {
1026
1033
  if (owner === null) {
@@ -1080,7 +1087,7 @@ class TypeLinker {
1080
1087
  return { text, links };
1081
1088
  }
1082
1089
  mention(text, from, scope, relation = "mentions") {
1083
- if (scope.typeParameters.has(text))
1090
+ if (scope.typeParameters.has(text) || scope.locals?.has(text))
1084
1091
  return null;
1085
1092
  const anywhere = { ...scope, sees: undefined };
1086
1093
  const qualified = QUALIFIED_NAME.test(text) ? text.split(/::|\./) : null;
@@ -1155,6 +1162,14 @@ var BINDING_TYPES = new Set([
1155
1162
  "lambda_literal",
1156
1163
  "closure_expression"
1157
1164
  ]);
1165
+ function sight(found, call, node) {
1166
+ const line = node.startPosition.row + 1;
1167
+ const known = found.get(keyOf(call));
1168
+ if (!known)
1169
+ found.set(keyOf(call), { call, lines: [line] });
1170
+ else if (!known.lines.includes(line))
1171
+ known.lines.push(line);
1172
+ }
1158
1173
  function readReferences(body, definitions) {
1159
1174
  const calls = new Map;
1160
1175
  const passes = new Map;
@@ -1167,12 +1182,12 @@ function readReferences(body, definitions) {
1167
1182
  if (CALL_TYPES.has(node.type)) {
1168
1183
  const call = callOf(node);
1169
1184
  if (call)
1170
- calls.set(keyOf(call), call);
1185
+ sight(calls, call, node);
1171
1186
  }
1172
1187
  for (const value of valuesOf(node)) {
1173
1188
  const passed = passedOf(value);
1174
1189
  if (passed)
1175
- passes.set(keyOf(passed), passed);
1190
+ sight(passes, passed, value);
1176
1191
  }
1177
1192
  const declared = LOCAL_DECLARATIONS[node.type];
1178
1193
  if (declared) {
@@ -1193,8 +1208,12 @@ function readReferences(body, definitions) {
1193
1208
  }
1194
1209
  }
1195
1210
  return {
1196
- calls: [...calls.values()].map((call) => call.via === "member" && call.qualifier.some((name) => locals.has(name)) ? { ...call, qualifier: [] } : call),
1197
- passes: [...passes.values()].filter((value) => !bindsName(bound, value))
1211
+ calls: [...calls.values()].map(({ call, lines }) => ({
1212
+ ...call,
1213
+ qualifier: call.via === "member" && call.qualifier.some((name) => locals.has(name)) ? [] : call.qualifier,
1214
+ lines: lines.sort((a, b) => a - b)
1215
+ })),
1216
+ passes: [...passes.values()].filter(({ call }) => !bindsName(bound, call)).map(({ call, lines }) => ({ ...call, lines: lines.sort((a, b) => a - b) }))
1198
1217
  };
1199
1218
  }
1200
1219
  function bindsName(bound, value) {
@@ -1335,7 +1354,7 @@ class CallLinker {
1335
1354
  const target = this.resolve(call, context);
1336
1355
  if (target === null || target === from || kind === "passes" && this.accessors.has(target))
1337
1356
  continue;
1338
- this.graph.link(from, kind, target);
1357
+ this.graph.link(from, kind, target, call.lines);
1339
1358
  }
1340
1359
  }
1341
1360
  registerAccessor(method) {
@@ -2706,6 +2725,7 @@ class SourceFileIndex {
2706
2725
  entryPoints = new Map;
2707
2726
  resolved = new Map;
2708
2727
  seen = new Map;
2728
+ declarations = new Map;
2709
2729
  add(path, ref, syntax, requests, exports) {
2710
2730
  const normalized = posix3.normalize(path);
2711
2731
  this.entries.set(normalized, { ref, syntax, requests, exports });
@@ -2739,6 +2759,12 @@ class SourceFileIndex {
2739
2759
  exportsOf(path) {
2740
2760
  return this.entries.get(posix3.normalize(path))?.exports ?? NO_EXPORTS;
2741
2761
  }
2762
+ exportsName(path, name) {
2763
+ const entry = this.entries.get(posix3.normalize(path));
2764
+ if (entry?.syntax?.readExports === undefined)
2765
+ return true;
2766
+ return entry.exports.declared.some((declared) => declared.name === name);
2767
+ }
2742
2768
  refOf(path) {
2743
2769
  return this.entries.get(posix3.normalize(path))?.ref;
2744
2770
  }
@@ -2755,17 +2781,72 @@ class SourceFileIndex {
2755
2781
  const known = this.resolved.get(normalized);
2756
2782
  if (known)
2757
2783
  return known;
2758
- const entry = this.entries.get(normalized);
2759
2784
  const imported = new Set;
2760
- for (const request of entry?.syntax ? entry.requests : []) {
2761
- const target = request.map((specifier) => entry?.syntax?.resolve(specifier, normalized, this) ?? null).find((found) => found !== null);
2762
- if (target && target !== normalized)
2763
- imported.add(target);
2764
- }
2785
+ for (const { file } of this.resolvedRequests(normalized))
2786
+ if (file !== normalized)
2787
+ imported.add(file);
2765
2788
  const result = [...imported];
2766
2789
  this.resolved.set(normalized, result);
2767
2790
  return result;
2768
2791
  }
2792
+ importLinksOf(path) {
2793
+ const normalized = posix3.normalize(path);
2794
+ const links = new Map;
2795
+ const add = (to, line) => {
2796
+ if (to !== undefined)
2797
+ links.set(to, [...links.get(to) ?? [], line]);
2798
+ };
2799
+ for (const { file, target, line } of this.resolvedRequests(normalized)) {
2800
+ if (file === normalized)
2801
+ continue;
2802
+ const fileRef = this.refOf(file);
2803
+ if (target.whole)
2804
+ add(fileRef, line);
2805
+ for (const name of target.names)
2806
+ add(this.declarationOf(file, name, new Set) ?? fileRef, line);
2807
+ }
2808
+ return [...links].map(([to, lines]) => ({ to, lines: [...new Set(lines)] }));
2809
+ }
2810
+ resolvedRequests(path) {
2811
+ const entry = this.entries.get(path);
2812
+ const syntax = entry?.syntax;
2813
+ if (!syntax)
2814
+ return [];
2815
+ return entry.requests.flatMap(({ targets, line }) => {
2816
+ for (const target of targets) {
2817
+ const file = syntax.resolve(target.specifier, path, this);
2818
+ if (file !== null)
2819
+ return [{ file, target, line }];
2820
+ }
2821
+ return [];
2822
+ });
2823
+ }
2824
+ declare(path, name, ref) {
2825
+ const normalized = posix3.normalize(path);
2826
+ const names = this.declarations.get(normalized) ?? new Map;
2827
+ if (!names.has(name))
2828
+ names.set(name, ref);
2829
+ this.declarations.set(normalized, names);
2830
+ }
2831
+ declarationOf(path, name, visited) {
2832
+ if (visited.has(path))
2833
+ return null;
2834
+ visited.add(path);
2835
+ const { declared, reexports } = this.exportsOf(path);
2836
+ const local = declared.find((entry) => entry.as === name);
2837
+ if (local)
2838
+ return this.declarations.get(path)?.get(local.name) ?? null;
2839
+ for (const { specifier, names } of reexports) {
2840
+ const handed = names === null ? name : names.find((entry) => entry.as === name)?.name;
2841
+ if (handed === undefined)
2842
+ continue;
2843
+ const target = this.resolveImport(specifier, path);
2844
+ const found = target === null ? null : this.declarationOf(target, handed, visited);
2845
+ if (found !== null)
2846
+ return found;
2847
+ }
2848
+ return this.entries.get(path)?.syntax?.readExports === undefined ? this.declarations.get(path)?.get(name) ?? null : null;
2849
+ }
2769
2850
  tracksImports(path) {
2770
2851
  return (this.entries.get(posix3.normalize(path))?.syntax ?? null) !== null;
2771
2852
  }
@@ -2811,7 +2892,7 @@ var C_INCLUDES = {
2811
2892
  if (!path)
2812
2893
  return [];
2813
2894
  const text = path.type === "system_lib_string" ? path.text.slice(1, -1) : path.text.replace(/^"|"$/g, "");
2814
- return text ? [[text]] : [];
2895
+ return text ? [{ targets: [{ specifier: text, whole: true, names: [] }], line: include.startPosition.row + 1 }] : [];
2815
2896
  }),
2816
2897
  resolve: (specifier, from, files) => {
2817
2898
  const local = besides(from, specifier);
@@ -2832,7 +2913,7 @@ var ECMASCRIPT_IMPORTS = {
2832
2913
  const source = node.type === "call_expression" ? dynamicSource(node) : node.childForFieldName("source") ?? requireClause(node)?.childForFieldName("source") ?? null;
2833
2914
  const text = source?.namedChildren.find((child) => child?.type === "string_fragment")?.text;
2834
2915
  if (text)
2835
- requests.push([text]);
2916
+ requests.push({ targets: [{ specifier: text, ...takenFrom(node) }], line: node.startPosition.row + 1 });
2836
2917
  }
2837
2918
  return requests;
2838
2919
  },
@@ -2877,6 +2958,20 @@ function packageBases(specifier, files) {
2877
2958
  const rest = segments.slice(nameLength).join("/") || "index";
2878
2959
  return [posix3.join(directory, "src", rest), posix3.join(directory, rest)];
2879
2960
  }
2961
+ function takenFrom(statement) {
2962
+ if (statement.type === "call_expression" || requireClause(statement) !== null)
2963
+ return { whole: true, names: [] };
2964
+ const clause = statement.namedChildren.find((child) => child?.type === "import_clause" || child?.type === "export_clause");
2965
+ if (!clause)
2966
+ return { whole: true, names: [] };
2967
+ const specifier = clause.type === "import_clause" ? "import_specifier" : "export_specifier";
2968
+ const names = clause.descendantsOfType(specifier).flatMap((entry) => {
2969
+ const name = entry?.childForFieldName("name")?.text;
2970
+ return name === undefined ? [] : [name];
2971
+ });
2972
+ const whole = clause.namedChildren.some((child) => child?.type === "identifier" || child?.type === "namespace_import");
2973
+ return { whole, names };
2974
+ }
2880
2975
  function requireClause(statement) {
2881
2976
  return statement.namedChildren.find((child) => child?.type === "import_require_clause") ?? null;
2882
2977
  }
@@ -2897,15 +2992,22 @@ var PYTHON_IMPORTS = {
2897
2992
  const dotted = name?.type === "aliased_import" ? name.childForFieldName("name") : name;
2898
2993
  return dotted ? [dotted.text] : [];
2899
2994
  });
2995
+ const line = node.startPosition.row + 1;
2900
2996
  if (node.type === "import_statement") {
2901
- requests.push(...names.map((name) => [name]));
2997
+ requests.push(...names.map((name) => ({ targets: [{ specifier: name, whole: true, names: [] }], line })));
2902
2998
  continue;
2903
2999
  }
2904
3000
  const module = node.childForFieldName("module_name")?.text;
2905
3001
  if (!module)
2906
3002
  continue;
2907
3003
  const separator = module.endsWith(".") ? "" : ".";
2908
- requests.push(...names.length > 0 ? names.map((name) => [`${module}${separator}${name}`, module]) : [[module]]);
3004
+ requests.push(...names.length > 0 ? names.map((name) => ({
3005
+ targets: [
3006
+ { specifier: `${module}${separator}${name}`, whole: true, names: [] },
3007
+ { specifier: module, whole: false, names: [name] }
3008
+ ],
3009
+ line
3010
+ })) : [{ targets: [{ specifier: module, whole: true, names: [] }], line }]);
2909
3011
  }
2910
3012
  return requests;
2911
3013
  },
@@ -3954,11 +4056,8 @@ class CodePlanner {
3954
4056
  return ref;
3955
4057
  }
3956
4058
  linkImports(from, path) {
3957
- for (const target of this.sources.importsOf(path)) {
3958
- const ref = this.sources.refOf(target);
3959
- if (ref !== undefined)
3960
- this.graph.link(from, "imports", ref);
3961
- }
4059
+ for (const { to, lines } of this.sources.importLinksOf(path))
4060
+ this.graph.link(from, "imports", to, lines);
3962
4061
  }
3963
4062
  async extract(file) {
3964
4063
  if (!this.registry.supports(file.path))
@@ -4064,6 +4163,7 @@ function terminate(style, statement) {
4064
4163
 
4065
4164
  // src/scanner/code/symbols.ts
4066
4165
  var CONSTRUCTIBLE2 = new Set(["class", "struct"]);
4166
+ var TEST_FILE = /(?:^|\/)(?:tests?|__tests__)\/|[._]test\.\w+$|\.spec\.\w+$|(?:^|\/)test_[^/]*\.py$/;
4067
4167
 
4068
4168
  class SymbolPlanner {
4069
4169
  graph;
@@ -4109,16 +4209,23 @@ class SymbolPlanner {
4109
4209
  const isConstructor = scope.parent !== null && spec.constructorNames(scope.parent.name).includes(symbol.name);
4110
4210
  const ownerDoc = isConstructor ? parseDoc(spec.docStyle, scope.parent?.comment ?? "") : null;
4111
4211
  const family = familyOf(spec);
4212
+ const parameters = plannedParameters(symbol, doc, ownerDoc);
4213
+ const fields = plannedFields(symbol, doc, spec);
4112
4214
  const typeScope = {
4113
4215
  path: file.path,
4114
4216
  family,
4115
4217
  typeParameters: new Set([...scope.typeParameters, ...symbol.typeParameters]),
4116
4218
  namespaceSeparator: spec.namespaceSeparator,
4117
- sees: (path) => this.sources.sees(file.path, path)
4219
+ sees: (path) => this.sources.sees(file.path, path),
4220
+ admits: (declaration) => this.admits(file.path, declaration),
4221
+ locals: new Set([...parameters, ...fields].map((declaration) => declaration.name))
4118
4222
  };
4119
4223
  const ref = this.graph.add(symbol.kind, { name: symbol.name, parent: scope.parentRef, anchor }, (self) => this.symbolContent(symbol, doc, scope, typeScope, self));
4120
4224
  const privateFile = spec.isPrivateFile?.(file.path) ?? false;
4121
- const site = { ref, path: file.path, family, owners: scope.owners, privateFile };
4225
+ const exported = this.sources.exportsName(file.path, scope.owners[0] ?? symbol.name);
4226
+ const site = { ref, path: file.path, family, owners: scope.owners, privateFile, exported };
4227
+ if (scope.parent === null)
4228
+ this.sources.declare(file.path, symbol.name, ref);
4122
4229
  if (TYPE_KINDS.has(symbol.kind) && !symbol.inferred) {
4123
4230
  const outOfLine = scope.parent !== null && scope.parent.inferred && TYPE_KINDS.has(scope.parent.kind);
4124
4231
  this.types.register(symbol.name, { ...site, local: privateFile && !outOfLine });
@@ -4130,10 +4237,10 @@ class SymbolPlanner {
4130
4237
  if (symbol.accessor)
4131
4238
  this.calls.registerAccessor(ref);
4132
4239
  }
4133
- for (const parameter of plannedParameters(symbol, doc, ownerDoc)) {
4240
+ for (const parameter of parameters) {
4134
4241
  this.graph.add("parameter", { name: parameter.name, parent: ref }, (self) => this.declarationContent(parameter, self, file, typeScope));
4135
4242
  }
4136
- for (const field of plannedFields(symbol, doc, spec)) {
4243
+ for (const field of fields) {
4137
4244
  this.graph.add("field", { name: field.name, parent: ref }, async (self) => ({
4138
4245
  ...await this.declarationContent(field, self, file, typeScope),
4139
4246
  annotations: field.annotations
@@ -4152,6 +4259,11 @@ class SymbolPlanner {
4152
4259
  for (const child of symbol.children)
4153
4260
  this.planSymbol(child, inner);
4154
4261
  }
4262
+ admits(from, declaration) {
4263
+ if (TEST_FILE.test(declaration.path) && !TEST_FILE.test(from))
4264
+ return false;
4265
+ return declaration.exported || this.sources.packageOf(declaration.path) === this.sources.packageOf(from);
4266
+ }
4155
4267
  async symbolContent(symbol, doc, scope, typeScope, self) {
4156
4268
  const { spec } = scope.file;
4157
4269
  if (symbol.heritage)
@@ -4953,8 +5065,10 @@ class GraphBuilder {
4953
5065
  const route = routeOf(this.identities, ref);
4954
5066
  return hrefOf(fragment ? { path: route.path, hash: fragment } : route);
4955
5067
  }
4956
- link(from, kind, to) {
4957
- this.edges.set(`${from}:${kind}:${to}`, [from, kind, to]);
5068
+ link(from, kind, to, lines = []) {
5069
+ const key = `${from}:${kind}:${to}`;
5070
+ const merged = [...new Set([...this.edges.get(key)?.[3] ?? [], ...lines])].sort((a, b) => a - b);
5071
+ this.edges.set(key, merged.length > 0 ? [from, kind, to, merged] : [from, kind, to]);
4958
5072
  }
4959
5073
  async build() {
4960
5074
  this.keepOnePackagePerDirectory();
@@ -8257,7 +8371,7 @@ async function prepareOutputDir(repo) {
8257
8371
  // package.json
8258
8372
  var package_default = {
8259
8373
  name: "@g3nomaly/analyze",
8260
- version: "0.1.6",
8374
+ version: "0.1.7",
8261
8375
  description: "Turns a repository into one offline HTML documentation site, and queries the same linked model from the terminal and from hooks.",
8262
8376
  keywords: [
8263
8377
  "documentation",
@@ -9410,19 +9524,51 @@ function stepsOf(graph, node, direction, kinds) {
9410
9524
  const members = graph.children(node).filter((child) => isDeclarationKind(graph.node(child).kind));
9411
9525
  const edges = [node, ...members].flatMap((end) => direction === "out" ? graph.edgesFrom(end) : graph.edgesTo(end)).filter((edge) => kinds.length === 0 || kinds.includes(edge[1]));
9412
9526
  const end = direction === "out" ? 2 : 0;
9413
- const seen = new Set;
9414
- return edges.flatMap((edge) => {
9527
+ const steps = new Map;
9528
+ for (const edge of edges) {
9415
9529
  const { owner } = ownerOf(graph, edge[end]);
9416
9530
  const via = edge[1] === "type" ? ownerOf(graph, edge[0]).via : null;
9417
9531
  const key = `${edge[1]} ${owner} ${via ?? ""}`;
9418
- if (seen.has(key))
9419
- return [];
9420
- seen.add(key);
9421
- return [{ relation: edge[1], node: owner, via }];
9422
- });
9532
+ const known = steps.get(key);
9533
+ const lines = [...new Set([...known?.lines ?? [], ...edge[3] ?? []])].sort((a, b) => a - b);
9534
+ steps.set(key, { relation: edge[1], node: owner, via, source: known?.source ?? edge[0], lines });
9535
+ }
9536
+ return [...steps.values()];
9537
+ }
9538
+ function fileOf(graph, node) {
9539
+ for (const ref of [node, ...graph.ancestors(node).reverse()]) {
9540
+ const value = graph.node(ref);
9541
+ if (value.kind === "source" || isFileNode(value))
9542
+ return ref;
9543
+ }
9544
+ return node;
9545
+ }
9546
+ function fileStepsOf(graph, file, direction, kinds) {
9547
+ const end = direction === "out" ? 2 : 0;
9548
+ const steps = new Map;
9549
+ for (const inner of [file, ...graph.descendants(file)]) {
9550
+ for (const edge of direction === "out" ? graph.edgesFrom(inner) : graph.edgesTo(inner)) {
9551
+ if (kinds.length > 0 && !kinds.includes(edge[1]))
9552
+ continue;
9553
+ const other = fileOf(graph, edge[end]);
9554
+ if (other === file)
9555
+ continue;
9556
+ const key = `${edge[1]} ${other}`;
9557
+ const known = steps.get(key);
9558
+ const lines = [...new Set([...known?.lines ?? [], ...edge[3] ?? []])].sort((a, b) => a - b);
9559
+ steps.set(key, { relation: edge[1], node: other, via: null, source: known?.source ?? edge[0], lines });
9560
+ }
9561
+ }
9562
+ return [...steps.values()];
9423
9563
  }
9424
9564
  function referenceOf(graph, step) {
9425
- return { relation: step.relation, node: summarize(graph, step.node), via: step.via };
9565
+ const path = step.lines.length > 0 ? pathOf(graph, step.source) : null;
9566
+ return {
9567
+ relation: step.relation,
9568
+ node: summarize(graph, step.node),
9569
+ via: step.via,
9570
+ location: path === null ? null : { path, lines: step.lines }
9571
+ };
9426
9572
  }
9427
9573
  function referencesOf(graph, node, options) {
9428
9574
  const { direction, kinds } = options;
@@ -9520,7 +9666,9 @@ function describeNode(graph, node) {
9520
9666
  };
9521
9667
  }
9522
9668
  // src/retrieval/walk.ts
9669
+ init_model();
9523
9670
  var DEFAULT_WALK_LIMIT = 50;
9671
+ var WALK_LEVELS = ["entries", "files"];
9524
9672
  function walkReferences(graph, start, options) {
9525
9673
  const directions = ["out", "in"].filter((direction) => options.direction === "both" || options.direction === direction).map((direction) => {
9526
9674
  const root = [];
@@ -9529,7 +9677,7 @@ function walkReferences(graph, start, options) {
9529
9677
  let left = options.limit;
9530
9678
  let omitted = 0;
9531
9679
  for (let hop = 1;hop <= options.depth; hop++) {
9532
- const candidates = directions.map((walk) => walk.frontier.flatMap(({ node, into }) => stepsOf(graph, node, walk.direction, options.kinds).map((step) => ({ step, into }))));
9680
+ const candidates = directions.map((walk) => walk.frontier.flatMap(({ node, into }) => (options.level === "files" ? fileStepsOf : stepsOf)(graph, node, walk.direction, options.kinds).map((step) => ({ step, into }))));
9533
9681
  const frontiers = directions.map(() => []);
9534
9682
  const longest = Math.max(0, ...candidates.map((list) => list.length));
9535
9683
  for (let index = 0;index < longest; index++) {
@@ -9563,19 +9711,27 @@ function walkReferences(graph, start, options) {
9563
9711
  omitted
9564
9712
  };
9565
9713
  }
9566
- function shortestPath(graph, from, to, kinds) {
9567
- const reached = new Map([[from, null]]);
9714
+ function shortestPath(graph, from, to, kinds, includeMentions = false) {
9715
+ const follows = kinds.length > 0 || includeMentions ? kinds : EDGE_KINDS.filter((kind) => kind !== "mentions");
9716
+ const reached = new Map([
9717
+ [from, null]
9718
+ ]);
9568
9719
  for (let frontier = [from];frontier.length > 0 && !reached.has(to); ) {
9569
- const next = [];
9720
+ const hop = new Map;
9570
9721
  for (const node of frontier) {
9571
- for (const step of stepsOf(graph, node, "out", kinds)) {
9722
+ const before = reached.get(node)?.mentions ?? 0;
9723
+ for (const step of stepsOf(graph, node, "out", follows)) {
9572
9724
  if (reached.has(step.node))
9573
9725
  continue;
9574
- reached.set(step.node, { previous: node, step });
9575
- next.push(step.node);
9726
+ const mentions = before + (step.relation === "mentions" ? 1 : 0);
9727
+ const known = hop.get(step.node);
9728
+ if (known === undefined || mentions < known.mentions)
9729
+ hop.set(step.node, { previous: node, step, mentions });
9576
9730
  }
9577
9731
  }
9578
- frontier = next;
9732
+ for (const [node, way] of hop)
9733
+ reached.set(node, way);
9734
+ frontier = [...hop.keys()];
9579
9735
  }
9580
9736
  if (!reached.has(to))
9581
9737
  return null;
@@ -9970,8 +10126,11 @@ class Retriever {
9970
10126
  walk(node, options) {
9971
10127
  return walkReferences(this.graph, node, options);
9972
10128
  }
9973
- path(from, to, kinds) {
9974
- return shortestPath(this.graph, from, to, kinds);
10129
+ fileOf(node) {
10130
+ return fileOf(this.graph, node);
10131
+ }
10132
+ path(from, to, kinds, includeMentions = false) {
10133
+ return shortestPath(this.graph, from, to, kinds, includeMentions);
9975
10134
  }
9976
10135
  exportGraph(options) {
9977
10136
  return exportGraph(this.graph, options);
@@ -10287,9 +10446,15 @@ ${wrap2(ALL_NODE_KINDS.join(", "), 21)}
10287
10446
  --outgoing refs: only what the node links to.
10288
10447
  --depth <n> refs: hops to follow from the node. Default: 1.
10289
10448
  --edge <kind> refs, path, graph: only edges of this kind, from the list below. Repeatable.
10449
+ path follows every kind but mentions when no --edge is given.
10450
+ --include-mentions path: also follow mentions, the links descriptions make. Of equally
10451
+ short chains, the one with the fewest mentions still wins.
10290
10452
  --level <level> graph: ${either(EXPORT_LEVELS)}. Default: ${DEFAULT_EXPORT_LEVEL}. packages and
10291
10453
  communities sum the edges between packages or communities; files, entries
10292
10454
  and all are the levels of the graph page of the site.
10455
+ refs: ${either(WALK_LEVELS)}. Default: entries. files lists the links between
10456
+ the file of the node and other files, as the files level of the graph page
10457
+ draws them.
10293
10458
 
10294
10459
  Environment: ANALYZE_REPO, ANALYZE_TITLE, ANALYZE_OUT, ANALYZE_SKILLS, ANALYZE_DOCS,
10295
10460
  ANALYZE_EXCLUDE, ANALYZE_EMBEDDINGS (on or off) and ANALYZE_FROM set the same options. A
@@ -10312,7 +10477,8 @@ var FLAG_COMMANDS = {
10312
10477
  outgoing: ["refs"],
10313
10478
  depth: ["refs"],
10314
10479
  edge: ["refs", "path", "graph"],
10315
- level: ["graph"]
10480
+ "include-mentions": ["path"],
10481
+ level: ["graph", "refs"]
10316
10482
  };
10317
10483
  var SCAN_FLAGS = ["repo", "skills", "docs", "exclude", "no-embeddings", "no-cache"];
10318
10484
  function expandHome(path) {
@@ -10419,7 +10585,7 @@ function parseInvocation(argv, env, cwd) {
10419
10585
  throw new UsageError("path takes two nodes. Run: g3n-analyze path <from> <to>, with quotes around a name with spaces.");
10420
10586
  }
10421
10587
  const [start = "", end = ""] = words;
10422
- return { ...base, kind: "path", start, end, edges };
10588
+ return { ...base, kind: "path", start, end, edges, includeMentions: values["include-mentions"] === true };
10423
10589
  }
10424
10590
  if (text.length === 0)
10425
10591
  throw new UsageError(`Missing node. Run: g3n-analyze ${command} <node>`);
@@ -10435,7 +10601,8 @@ function parseInvocation(argv, env, cwd) {
10435
10601
  direction: values.incoming ? "in" : values.outgoing ? "out" : "both",
10436
10602
  kinds: edges,
10437
10603
  depth: countOf(values.depth, "depth") ?? 1,
10438
- limit: countOf(values.limit, "limit") ?? DEFAULT_WALK_LIMIT
10604
+ limit: countOf(values.limit, "limit") ?? DEFAULT_WALK_LIMIT,
10605
+ level: values.level === undefined ? "entries" : oneOf(values.level, WALK_LEVELS, "level")
10439
10606
  }
10440
10607
  };
10441
10608
  }
@@ -10475,6 +10642,7 @@ function parse(argv) {
10475
10642
  depth: { type: "string" },
10476
10643
  edge: { type: "string", multiple: true },
10477
10644
  level: { type: "string" },
10645
+ "include-mentions": { type: "boolean" },
10478
10646
  help: { type: "boolean", short: "h" },
10479
10647
  version: { type: "boolean", short: "v" }
10480
10648
  }
@@ -10536,16 +10704,23 @@ var pathSchema = z11.strictObject({
10536
10704
  ...warningShape,
10537
10705
  start: z11.string().trim().min(1),
10538
10706
  end: z11.string().trim().min(1),
10539
- edges: z11.array(z11.enum(EDGE_KINDS)).readonly().optional()
10707
+ edges: z11.array(z11.enum(EDGE_KINDS)).readonly().optional(),
10708
+ includeMentions: z11.boolean().optional()
10540
10709
  }).superRefine(checkSource).transform((fields) => ({
10541
10710
  source: sourceOf(fields),
10542
10711
  reporter: reporterOf(fields.onWarning),
10543
- request: { start: fields.start, end: fields.end, filter: filterOf(fields), kinds: fields.edges ?? [] }
10712
+ request: {
10713
+ start: fields.start,
10714
+ end: fields.end,
10715
+ filter: filterOf(fields),
10716
+ kinds: fields.edges ?? [],
10717
+ includeMentions: fields.includeMentions ?? false
10718
+ }
10544
10719
  }));
10545
10720
  function pathIn(retriever, request) {
10546
10721
  const start = nodeOf(retriever, request.start, request.filter);
10547
10722
  const end = nodeOf(retriever, request.end, request.filter);
10548
- const steps = retriever.path(start, end, request.kinds);
10723
+ const steps = retriever.path(start, end, request.kinds, request.includeMentions);
10549
10724
  if (steps === null) {
10550
10725
  const edges = request.kinds.length > 0 ? ` along ${request.kinds.join(", ")} edges` : "";
10551
10726
  throw new QueryError(`No path from "${request.start}" to "${request.end}"${edges}. A path follows edges the way they point, so try the other order.`);
@@ -10569,7 +10744,8 @@ var refsSchema = z12.strictObject({
10569
10744
  outgoing: z12.boolean().optional(),
10570
10745
  edges: z12.array(z12.enum(EDGE_KINDS)).readonly().optional(),
10571
10746
  depth: z12.number().int().positive().optional(),
10572
- limit: z12.number().int().positive().optional()
10747
+ limit: z12.number().int().positive().optional(),
10748
+ level: z12.enum(WALK_LEVELS).optional()
10573
10749
  }).superRefine(checkSource).refine((fields) => !(fields.incoming && fields.outgoing), {
10574
10750
  message: "incoming and outgoing exclude each other",
10575
10751
  path: ["outgoing"]
@@ -10583,12 +10759,14 @@ var refsSchema = z12.strictObject({
10583
10759
  direction: fields.incoming ? "in" : fields.outgoing ? "out" : "both",
10584
10760
  kinds: fields.edges ?? [],
10585
10761
  depth: fields.depth ?? 1,
10586
- limit: fields.limit ?? DEFAULT_WALK_LIMIT
10762
+ limit: fields.limit ?? DEFAULT_WALK_LIMIT,
10763
+ level: fields.level ?? "entries"
10587
10764
  }
10588
10765
  }
10589
10766
  }));
10590
10767
  function refsIn(retriever, request) {
10591
- const node = nodeOf(retriever, request.target, request.filter);
10768
+ const target = nodeOf(retriever, request.target, request.filter);
10769
+ const node = request.walk.level === "files" ? retriever.fileOf(target) : target;
10592
10770
  return { node: retriever.summarize(node), ...retriever.walk(node, request.walk) };
10593
10771
  }
10594
10772
  async function refs(options) {
@@ -10618,6 +10796,7 @@ async function show(options) {
10618
10796
 
10619
10797
  // src/cli/render-text.ts
10620
10798
  init_model();
10799
+ import { posix as posix26 } from "path";
10621
10800
  function placeOf(node) {
10622
10801
  if (node.path === null)
10623
10802
  return "";
@@ -10650,8 +10829,13 @@ function summaryLine(node) {
10650
10829
  return node.summary ? `${head}: ${node.summary}` : head;
10651
10830
  }
10652
10831
  function referenceLine(reference) {
10653
- const { relation, via, node } = reference;
10654
- return `- ${relation}${via === null ? "" : ` via \`${via}\``}: ${labelOf(node)} (\`${node.id}\`)`;
10832
+ const { node } = reference;
10833
+ return `- ${relationText(reference)}: ${labelOf(node)} (\`${node.id}\`)`;
10834
+ }
10835
+ function relationText({ relation, via, location }) {
10836
+ const through = via === null ? "" : ` via \`${via}\``;
10837
+ const at = location === null ? "" : ` (${posix26.basename(location.path)}:${location.lines.join(", ")})`;
10838
+ return `${relation}${through}${at}`;
10655
10839
  }
10656
10840
  function section(title, lines) {
10657
10841
  return lines.length === 0 ? [] : [`## ${title}`, lines.join(`
@@ -10671,15 +10855,22 @@ function referenceSections(references) {
10671
10855
  ...section(`Linked from (${references.linkedFrom.length})`, references.linkedFrom.map(referenceLine))
10672
10856
  ];
10673
10857
  }
10674
- function walkLines(reference, depth) {
10675
- return [`${" ".repeat(depth)}${referenceLine(reference)}`, ...reference.next.flatMap((next) => walkLines(next, depth + 1))];
10676
- }
10677
- function walkSize(reference) {
10678
- return 1 + reference.next.reduce((sum, next) => sum + walkSize(next), 0);
10858
+ function listLines(references, depth) {
10859
+ const byNode = new Map;
10860
+ for (const reference of references)
10861
+ byNode.set(reference.node.id, [...byNode.get(reference.node.id) ?? [], reference]);
10862
+ return [...byNode.values()].flatMap((group) => {
10863
+ const [first] = group;
10864
+ if (!first)
10865
+ return [];
10866
+ const relations = group.map(relationText).join(", ");
10867
+ const line = `${" ".repeat(depth)}- ${relations}: ${labelOf(first.node)} (\`${first.node.id}\`)`;
10868
+ return [line, ...listLines(group.flatMap((reference) => reference.next), depth + 1)];
10869
+ });
10679
10870
  }
10680
10871
  function walkSection(title, references) {
10681
- const count = references.reduce((sum, reference) => sum + walkSize(reference), 0);
10682
- return section(`${title} (${count})`, references.flatMap((reference) => walkLines(reference, 0)));
10872
+ const lines = listLines(references, 0);
10873
+ return section(`${title} (${lines.length})`, lines);
10683
10874
  }
10684
10875
  function contentText(content) {
10685
10876
  return content.map(({ mediaType, schema }) => schema === null ? mediaType : `${mediaType}: \`${schema}\``).join(", ");
@@ -10806,7 +10997,7 @@ function renderPath(result) {
10806
10997
  const title = `# path ${labelOf(start)} -> ${labelOf(end)} (${steps.length} ${steps.length === 1 ? "step" : "steps"})`;
10807
10998
  const lines = [
10808
10999
  `${labelOf(start)} (\`${start.id}\`)`,
10809
- ...steps.map(({ relation, via, node }) => ` -${relation}${via === null ? "" : ` via \`${via}\``}-> ${labelOf(node)} (\`${node.id}\`)`)
11000
+ ...steps.map((step) => ` -${relationText(step)}-> ${labelOf(step.node)} (\`${step.node.id}\`)`)
10810
11001
  ];
10811
11002
  return [title, lines.join(`
10812
11003
  `)].join(`
@@ -10911,13 +11102,13 @@ async function runQuery(invocation, onWarning) {
10911
11102
  const { target, walk } = invocation;
10912
11103
  const incoming = walk.direction === "in";
10913
11104
  const outgoing = walk.direction === "out";
10914
- const { depth, limit } = walk;
10915
- const result = await refs({ ...common, target, incoming, outgoing, edges: walk.kinds, depth, limit });
11105
+ const { depth, limit, level } = walk;
11106
+ const result = await refs({ ...common, target, incoming, outgoing, edges: walk.kinds, depth, limit, level });
10916
11107
  return outputOf({ kind: "refs", result });
10917
11108
  }
10918
11109
  case "path": {
10919
- const { start, end, edges } = invocation;
10920
- return outputOf({ kind: "path", result: await path2({ ...common, start, end, edges }) });
11110
+ const { start, end, edges, includeMentions } = invocation;
11111
+ return outputOf({ kind: "path", result: await path2({ ...common, start, end, edges, includeMentions }) });
10921
11112
  }
10922
11113
  case "list":
10923
11114
  return outputOf({ kind: "list", result: await list2({ ...common, limit: invocation.limit ?? undefined }) });