codeabc 0.1.0__py3-none-any.whl

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.
Files changed (62) hide show
  1. backend/__init__.py +0 -0
  2. backend/app.py +95 -0
  3. backend/desktop_server.py +46 -0
  4. backend/models.py +629 -0
  5. backend/prompts/__init__.py +0 -0
  6. backend/prompts/annotate.py +44 -0
  7. backend/prompts/edit.py +62 -0
  8. backend/prompts/overview.py +57 -0
  9. backend/prompts/qa.py +45 -0
  10. backend/routers/__init__.py +0 -0
  11. backend/routers/analyze.py +271 -0
  12. backend/routers/project.py +872 -0
  13. backend/services/__init__.py +0 -0
  14. backend/services/action_plan.py +366 -0
  15. backend/services/activity.py +313 -0
  16. backend/services/apimap.py +391 -0
  17. backend/services/async_surface.py +123 -0
  18. backend/services/cache.py +183 -0
  19. backend/services/churn.py +244 -0
  20. backend/services/ci_checks.py +317 -0
  21. backend/services/codemap_export.py +136 -0
  22. backend/services/commands.py +276 -0
  23. backend/services/complexity.py +111 -0
  24. backend/services/config_files.py +190 -0
  25. backend/services/contributing.py +308 -0
  26. backend/services/coverage.py +283 -0
  27. backend/services/datamodels.py +190 -0
  28. backend/services/deep_nesting.py +110 -0
  29. backend/services/dependencies.py +270 -0
  30. backend/services/docs.py +255 -0
  31. backend/services/docstrings.py +163 -0
  32. backend/services/duplicate_code.py +165 -0
  33. backend/services/entrypoints.py +152 -0
  34. backend/services/envscan.py +167 -0
  35. backend/services/error_handling.py +239 -0
  36. backend/services/filenames.py +261 -0
  37. backend/services/github_clone.py +129 -0
  38. backend/services/glossary.py +96 -0
  39. backend/services/health_score.py +381 -0
  40. backend/services/importgraph.py +986 -0
  41. backend/services/integrations.py +263 -0
  42. backend/services/licenses.py +505 -0
  43. backend/services/llm.py +117 -0
  44. backend/services/long_functions.py +101 -0
  45. backend/services/ownership.py +194 -0
  46. backend/services/package_purposes.py +112 -0
  47. backend/services/pr_reading.py +181 -0
  48. backend/services/release_map.py +402 -0
  49. backend/services/report_export.py +232 -0
  50. backend/services/risk.py +88 -0
  51. backend/services/scanner.py +546 -0
  52. backend/services/schedules.py +369 -0
  53. backend/services/security.py +445 -0
  54. backend/services/settings_map.py +234 -0
  55. backend/services/symbols.py +605 -0
  56. backend/services/techdebt.py +124 -0
  57. backend/services/too_many_params.py +102 -0
  58. backend/services/typing_coverage.py +184 -0
  59. codeabc-0.1.0.dist-info/METADATA +283 -0
  60. codeabc-0.1.0.dist-info/RECORD +62 -0
  61. codeabc-0.1.0.dist-info/WHEEL +4 -0
  62. codeabc-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,605 @@
1
+ """Definition index — where each name in the code is actually defined.
2
+
3
+ Reading unfamiliar code means constantly hitting a name and wondering "where
4
+ does this come from?". A call to ``parse_config``, a ``class Scanner``, an
5
+ imported helper — the reader wants the file and line that defines it, not a web
6
+ search. This module builds that index from the source CodeABC already loaded,
7
+ with no LLM and no language server: it scans for top-level definitions (and one
8
+ level of class methods) and records where each one lives, so the UI can offer
9
+ "jump to definition" for any name a reader clicks.
10
+
11
+ It covers the two languages CodeABC annotates today:
12
+
13
+ Python ``def`` / ``async def`` / ``class`` at module level, plus methods
14
+ one indent inside a class (recorded with their enclosing class).
15
+ JS / TS ``function`` / ``async function`` / ``export function`` / ``class``,
16
+ ``const name = (...) =>`` / ``const name = function`` assignments, and
17
+ methods one indent inside a class (recorded with their class).
18
+
19
+ Each entry carries the name, its kind (function / class / method), the
20
+ enclosing class for a method, the file, and a 1-based line number.
21
+
22
+ Limitations (kept honest on purpose):
23
+
24
+ * Matching is regex over indentation, not a real parser. Decorators that
25
+ rename, ``setattr`` / ``globals()`` tricks, conditional re-exports, and
26
+ names built at runtime are out of scope.
27
+ * A name defined in several files yields one entry per file; the caller
28
+ decides which is meant.
29
+
30
+ The companion lookup :func:`find_references` answers the other half — where a
31
+ name is *used*, not just where it is born — by whole-word text search across the
32
+ same source files, flagging the occurrence that is the declaration itself.
33
+
34
+ Every entry also carries an ``exported`` flag — public by Python's underscore
35
+ convention or a JS/TS ``export`` — and :func:`public_api` filters the index down
36
+ to just that surface, so a reader can see a project's interface before its
37
+ internals.
38
+ """
39
+
40
+ from __future__ import annotations
41
+
42
+ import re
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Shared helpers
46
+ # ---------------------------------------------------------------------------
47
+
48
+ _IDENT = r"[A-Za-z_$][\w$]*"
49
+
50
+
51
+ def _lang(path: str) -> str:
52
+ ext = path.rsplit(".", 1)[-1].lower() if "." in path else ""
53
+ if ext == "py":
54
+ return "python"
55
+ if ext in ("js", "ts", "jsx", "tsx", "mjs", "cjs"):
56
+ return "js"
57
+ return ext
58
+
59
+
60
+ def _indent_width(line: str) -> int:
61
+ expanded = line.expandtabs(4)
62
+ return len(expanded) - len(expanded.lstrip())
63
+
64
+
65
+ def _count_phrase(n: int, kind: str) -> str:
66
+ """'1 class', '2 classes', '1 function' — grammatical for a plain-language note."""
67
+ if kind == "class":
68
+ word = "class" if n == 1 else "classes"
69
+ else:
70
+ word = kind if n == 1 else f"{kind}s"
71
+ return f"{n} {word}"
72
+
73
+
74
+ # ---------------------------------------------------------------------------
75
+ # Python
76
+ # ---------------------------------------------------------------------------
77
+
78
+ _PY_DEF_RE = re.compile(r"^(?P<indent>\s*)(?:async\s+)?def\s+(?P<name>\w+)\s*\(")
79
+ _PY_CLASS_RE = re.compile(r"^(?P<indent>\s*)class\s+(?P<name>\w+)\s*[:(]")
80
+
81
+
82
+ def _py_definitions(path: str, content: str) -> list[dict]:
83
+ """Top-level functions/classes plus direct (and nested-class) methods.
84
+
85
+ A stack of open ``class`` / ``def`` blocks, keyed by indentation, tells us
86
+ what encloses each definition. A ``def`` whose nearest enclosing block is a
87
+ class is a method; a ``def`` nested inside another ``def`` is a local helper
88
+ and is skipped (it is not part of the public reading surface).
89
+ """
90
+ out: list[dict] = []
91
+ # frames: (indent, kind, name, exported) for currently open class/def blocks
92
+ frames: list[tuple[int, str, str, bool]] = []
93
+
94
+ for lineno, raw in enumerate(content.splitlines(), start=1):
95
+ stripped = raw.strip()
96
+ if not stripped or stripped.startswith("#"):
97
+ continue
98
+
99
+ class_match = _PY_CLASS_RE.match(raw)
100
+ def_match = None if class_match else _PY_DEF_RE.match(raw)
101
+ if class_match is None and def_match is None:
102
+ continue
103
+
104
+ indent = _indent_width(raw)
105
+ # close every block that this line is not nested inside
106
+ while frames and frames[-1][0] >= indent:
107
+ frames.pop()
108
+ enclosing = frames[-1] if frames else None
109
+ # A name is part of the public surface only when its container is and it
110
+ # follows the convention: no leading underscore.
111
+ parent_exported = _enclosing_exported(enclosing)
112
+
113
+ if class_match is not None:
114
+ name = class_match.group("name")
115
+ parent = enclosing[2] if enclosing and enclosing[1] == "class" else None
116
+ exported = parent_exported and not name.startswith("_")
117
+ out.append(_entry(path, "python", name, "class", parent, lineno, exported))
118
+ frames.append((indent, "class", name, exported))
119
+ continue
120
+
121
+ assert def_match is not None # narrowing: class_match is None here
122
+ name = def_match.group("name")
123
+ exported = parent_exported and not name.startswith("_")
124
+ if enclosing is None:
125
+ out.append(_entry(path, "python", name, "function", None, lineno, exported))
126
+ frames.append((indent, "def", name, exported))
127
+ elif enclosing[1] == "class":
128
+ out.append(_entry(path, "python", name, "method", enclosing[2], lineno, exported))
129
+ frames.append((indent, "def", name, exported))
130
+ else:
131
+ # nested inside another def: a local helper — track it so its own
132
+ # nested blocks resolve correctly, but do not index it.
133
+ frames.append((indent, "def", name, False))
134
+
135
+ return out
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # JavaScript / TypeScript
140
+ # ---------------------------------------------------------------------------
141
+
142
+ _JS_FUNC_RE = re.compile(
143
+ r"^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*(" + _IDENT + r")\s*\("
144
+ )
145
+ _JS_CLASS_RE = re.compile(
146
+ r"^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+(" + _IDENT + r")\b"
147
+ )
148
+ # const/let/var name = (...) => ... or = async (...) => or = function
149
+ _JS_ASSIGN_RE = re.compile(
150
+ r"^\s*(?:export\s+)?(?:default\s+)?(?:const|let|var)\s+(" + _IDENT + r")\s*=\s*"
151
+ r"(?:async\s+)?(?:function\b|\*?\s*\([^)]*\)\s*=>|" + _IDENT + r"\s*=>)"
152
+ )
153
+ # A method *declaration* inside a class body: optional TS modifiers, an optional
154
+ # get/set or generator marker, the name, a parameter list, an optional TS return
155
+ # annotation, and the opening brace on the same line. The trailing ``{`` is what
156
+ # separates a declaration (``render() {``) from a call (``render();``).
157
+ _JS_METHOD_RE = re.compile(
158
+ r"^\s*(?:(?:public|private|protected|readonly|static|async|override|abstract)\s+)*"
159
+ r"(?:(?:get|set)\s+)?\*?\s*"
160
+ r"(?P<name>" + _IDENT + r")\s*\([^;]*\)\s*(?::[^={};]+)?\{"
161
+ )
162
+ # Control-flow heads also read as ``word (...) {``; never treat them as methods.
163
+ _JS_NON_METHODS = frozenset(
164
+ {"if", "for", "while", "switch", "catch", "do", "with", "return", "function", "else"}
165
+ )
166
+ # A top-level declaration is part of the public surface only when it is exported.
167
+ _JS_EXPORT_RE = re.compile(r"^\s*export\b")
168
+
169
+
170
+ def _js_line_exported(raw: str) -> bool:
171
+ """True when a declaration line begins with ``export`` (``export default`` too)."""
172
+ return bool(_JS_EXPORT_RE.match(raw))
173
+
174
+
175
+ def _js_method_private(raw: str) -> bool:
176
+ """True when a method carries a ``private`` / ``protected`` TS modifier.
177
+
178
+ Modifiers sit before the name and parameter list, so the words ahead of the
179
+ first ``(`` are enough to decide; ``#``-prefixed private fields never reach
180
+ here because they are not matched as methods in the first place.
181
+ """
182
+ prefix = raw.strip().split("(", 1)[0]
183
+ tokens = prefix.split()
184
+ return "private" in tokens or "protected" in tokens
185
+
186
+
187
+ def _js_definitions(path: str, content: str) -> list[dict]:
188
+ out: list[dict] = []
189
+ # frames: (indent, kind, name, exported) for currently open class/block scopes,
190
+ # mirroring the Python pass so a method is only read inside a class body and a
191
+ # method's own body is not re-scanned for nested "methods".
192
+ frames: list[tuple[int, str, str, bool]] = []
193
+
194
+ for lineno, raw in enumerate(content.splitlines(), start=1):
195
+ stripped = raw.strip()
196
+ if not stripped or stripped.startswith("//"):
197
+ continue
198
+
199
+ indent = _indent_width(raw)
200
+ while frames and frames[-1][0] >= indent:
201
+ frames.pop()
202
+ enclosing = frames[-1] if frames else None
203
+ # In JS/TS the public surface is what the module exports, not a naming
204
+ # convention; a member is only reachable when its container is too.
205
+ parent_exported = _enclosing_exported(enclosing)
206
+
207
+ class_match = _JS_CLASS_RE.match(raw)
208
+ if class_match:
209
+ name = class_match.group(1)
210
+ parent = enclosing[2] if enclosing and enclosing[1] == "class" else None
211
+ exported = parent_exported and _js_line_exported(raw)
212
+ out.append(_entry(path, "js", name, "class", parent, lineno, exported))
213
+ frames.append((indent, "class", name, exported))
214
+ continue
215
+
216
+ func_match = _JS_FUNC_RE.match(raw)
217
+ if func_match:
218
+ name = func_match.group(1)
219
+ exported = parent_exported and _js_line_exported(raw)
220
+ out.append(_entry(path, "js", name, "function", None, lineno, exported))
221
+ frames.append((indent, "block", name, exported))
222
+ continue
223
+
224
+ assign_match = _JS_ASSIGN_RE.match(raw)
225
+ if assign_match:
226
+ name = assign_match.group(1)
227
+ exported = parent_exported and _js_line_exported(raw)
228
+ out.append(_entry(path, "js", name, "function", None, lineno, exported))
229
+ frames.append((indent, "block", name, exported))
230
+ continue
231
+
232
+ if enclosing is not None and enclosing[1] == "class":
233
+ method_match = _JS_METHOD_RE.match(raw)
234
+ if method_match:
235
+ name = method_match.group("name")
236
+ if name not in _JS_NON_METHODS:
237
+ exported = parent_exported and not _js_method_private(raw)
238
+ out.append(_entry(path, "js", name, "method", enclosing[2], lineno, exported))
239
+ frames.append((indent, "block", name, exported))
240
+
241
+ return out
242
+
243
+
244
+ # ---------------------------------------------------------------------------
245
+ # Assembly
246
+ # ---------------------------------------------------------------------------
247
+
248
+
249
+ def _entry(
250
+ path: str,
251
+ lang: str,
252
+ name: str,
253
+ kind: str,
254
+ parent: str | None,
255
+ line: int,
256
+ exported: bool,
257
+ ) -> dict:
258
+ qualname = f"{parent}.{name}" if parent else name
259
+ return {
260
+ "name": name,
261
+ "qualname": qualname,
262
+ "kind": kind,
263
+ "parent": parent,
264
+ "lang": lang,
265
+ "file": path,
266
+ "line": line,
267
+ "exported": exported,
268
+ }
269
+
270
+
271
+ def _enclosing_exported(enclosing: tuple | None) -> bool:
272
+ """Whether a definition directly inside ``enclosing`` can be public.
273
+
274
+ Module level (no enclosing frame) and the body of a public class can hold
275
+ public names. The body of a function — or of a non-public class — cannot,
276
+ so a name born there is internal no matter what it is called.
277
+ """
278
+ if enclosing is None:
279
+ return True
280
+ if enclosing[1] == "class":
281
+ return bool(enclosing[3])
282
+ return False
283
+
284
+
285
+ def _build_notes(definitions: list[dict], file_contents: dict[str, str]) -> list[str]:
286
+ notes: list[str] = []
287
+ scanned = sum(1 for p in file_contents if _lang(p) in ("python", "js"))
288
+ if scanned == 0:
289
+ notes.append("No Python or JS/TS files were found to index.")
290
+ return notes
291
+
292
+ by_kind: dict[str, int] = {}
293
+ for d in definitions:
294
+ by_kind[d["kind"]] = by_kind.get(d["kind"], 0) + 1
295
+ parts = [_count_phrase(by_kind[k], k) for k in sorted(by_kind)]
296
+ if parts:
297
+ notes.append(f"Indexed {', '.join(parts)} across {scanned} source file(s).")
298
+
299
+ duplicates = _duplicate_names(definitions)
300
+ if duplicates:
301
+ sample = ", ".join(duplicates[:5])
302
+ notes.append(
303
+ f"{len(duplicates)} name(s) are defined in more than one place "
304
+ f"(e.g. {sample}); a lookup returns every match."
305
+ )
306
+
307
+ notes.append(
308
+ "Definitions only: this index shows where a name is declared, not "
309
+ "every place it is used. Matching is regex-based, so runtime-built or "
310
+ "re-exported names may be missed."
311
+ )
312
+ return notes
313
+
314
+
315
+ def _duplicate_names(definitions: list[dict]) -> list[str]:
316
+ seen: dict[str, int] = {}
317
+ for d in definitions:
318
+ seen[d["name"]] = seen.get(d["name"], 0) + 1
319
+ return sorted(name for name, count in seen.items() if count > 1)
320
+
321
+
322
+ def build_definition_index(file_contents: dict[str, str], *, limit: int = 2000) -> dict:
323
+ """Index every top-level definition (and class method) in the project.
324
+
325
+ Returns ``{"total", "definitions", "notes"}`` where ``definitions`` is a
326
+ deterministic, alphabetically sorted list of entries (see :func:`_entry`),
327
+ truncated to ``limit``.
328
+ """
329
+ definitions: list[dict] = []
330
+ for path in sorted(file_contents):
331
+ lang = _lang(path)
332
+ content = file_contents[path]
333
+ if lang == "python":
334
+ definitions.extend(_py_definitions(path, content))
335
+ elif lang == "js":
336
+ definitions.extend(_js_definitions(path, content))
337
+
338
+ definitions.sort(key=lambda d: (d["name"].lower(), d["file"], d["line"]))
339
+ notes = _build_notes(definitions, file_contents)
340
+ total = len(definitions)
341
+ return {"total": total, "definitions": definitions[:limit], "notes": notes}
342
+
343
+
344
+ def find_definition(file_contents: dict[str, str], name: str, *, limit: int = 50) -> list[dict]:
345
+ """Return the places that define ``name``.
346
+
347
+ Tries an exact match first, then falls back to a case-insensitive match so
348
+ a reader who types ``scanner`` still finds ``Scanner``. Results are ordered
349
+ by file then line.
350
+ """
351
+ target = (name or "").strip()
352
+ if not target:
353
+ return []
354
+
355
+ index = build_definition_index(file_contents, limit=100_000)
356
+ matches = [d for d in index["definitions"] if d["name"] == target]
357
+ if not matches:
358
+ lowered = target.lower()
359
+ matches = [d for d in index["definitions"] if d["name"].lower() == lowered]
360
+
361
+ matches.sort(key=lambda d: (d["file"], d["line"]))
362
+ return matches[:limit]
363
+
364
+
365
+ # ---------------------------------------------------------------------------
366
+ # Public surface — the names a project means to expose, not its internals
367
+ # ---------------------------------------------------------------------------
368
+
369
+
370
+ def public_api(file_contents: dict[str, str], *, limit: int = 2000) -> dict:
371
+ """Return the project's public surface — the names other code is meant to call.
372
+
373
+ Where :func:`build_definition_index` lists *every* definition, this keeps
374
+ only the ones a project exposes on purpose, so a reader sees the interface
375
+ before the internals. The rule follows each language's own convention:
376
+
377
+ Python a name is public unless it starts with an underscore, and a
378
+ method is public only when its class is too — so ``_helper`` and
379
+ ``Scanner.__init__`` are internal.
380
+ JS / TS a name is public when it is ``export``-ed, and a method is public
381
+ when its class is exported and it is not declared ``private`` /
382
+ ``protected``.
383
+
384
+ Returns ``{"total", "definitions", "notes"}`` with the same entry shape as
385
+ the definition index (each entry already carries an ``exported`` flag),
386
+ alphabetically sorted and truncated to ``limit``.
387
+ """
388
+ index = build_definition_index(file_contents, limit=100_000)
389
+ public = [d for d in index["definitions"] if d.get("exported")]
390
+ total = len(public)
391
+ notes = _public_api_notes(public, total, file_contents)
392
+ return {"total": total, "definitions": public[:limit], "notes": notes}
393
+
394
+
395
+ def _public_api_notes(public: list[dict], total: int, file_contents: dict[str, str]) -> list[str]:
396
+ langs = {_lang(p) for p in file_contents}
397
+ has_py = "python" in langs
398
+ has_js = "js" in langs
399
+ if not (has_py or has_js):
400
+ return ["No Python or JS/TS files were found to index."]
401
+ if total == 0:
402
+ return [
403
+ "No public names were found: every definition is underscore-prefixed "
404
+ "(Python) or not exported (JS/TS)."
405
+ ]
406
+
407
+ by_kind: dict[str, int] = {}
408
+ for d in public:
409
+ by_kind[d["kind"]] = by_kind.get(d["kind"], 0) + 1
410
+ parts = [_count_phrase(by_kind[k], k) for k in sorted(by_kind)]
411
+ notes = [f"{total} public name(s) make up the surface: {', '.join(parts)}."]
412
+
413
+ rules: list[str] = []
414
+ if has_py:
415
+ rules.append("Python counts a name as public when it has no leading underscore")
416
+ if has_js:
417
+ rules.append("JS/TS counts a name as public when it is exported")
418
+ notes.append(f"{'; '.join(rules)}.")
419
+
420
+ notes.append(
421
+ "Convention-based: a name shown here may still be internal (exported only "
422
+ "for tests, say), and __all__ or re-export lists are not consulted."
423
+ )
424
+ return notes
425
+
426
+
427
+ # ---------------------------------------------------------------------------
428
+ # References — where a name is used, not just where it is defined
429
+ # ---------------------------------------------------------------------------
430
+
431
+
432
+ def find_references(file_contents: dict[str, str], name: str, *, limit: int = 200) -> dict:
433
+ """Return every place ``name`` appears as a whole word — its use sites.
434
+
435
+ This is the other half of :func:`find_definition`: where that points at the
436
+ one line a name is born, this lists every line it shows up on, with a
437
+ trimmed one-line preview and an ``is_definition`` flag so the UI can tell the
438
+ declaration apart from the call sites. Matching is whole-word and
439
+ case-sensitive (``scan`` does not match ``scanner``, ``rescan`` or
440
+ ``my_scan``), and an attribute access like ``self.scan`` counts as a use.
441
+
442
+ Returns ``{"name", "total", "files", "references", "notes"}`` where
443
+ ``references`` is ordered by file then line and truncated to ``limit`` while
444
+ ``total`` and ``files`` count every match.
445
+
446
+ Limitations (kept honest, same as the definition index): it is text search,
447
+ not a parser. A use hidden inside a string literal or a comment may be
448
+ missed, and a same-named attribute on an unrelated object may be counted.
449
+ """
450
+ target = (name or "").strip()
451
+ if not target:
452
+ return {"name": name, "total": 0, "files": 0, "references": [], "notes": []}
453
+
454
+ pattern = re.compile(r"(?<![\w$])" + re.escape(target) + r"(?![\w$])")
455
+
456
+ index = build_definition_index(file_contents, limit=100_000)
457
+ def_sites = {(d["file"], d["line"]) for d in index["definitions"] if d["name"] == target}
458
+
459
+ references: list[dict] = []
460
+ for path in sorted(file_contents):
461
+ if _lang(path) not in ("python", "js"):
462
+ continue
463
+ for lineno, raw in enumerate(file_contents[path].splitlines(), start=1):
464
+ stripped = raw.strip()
465
+ if not stripped or stripped.startswith("#") or stripped.startswith("//"):
466
+ continue
467
+ if not pattern.search(raw):
468
+ continue
469
+ references.append(
470
+ {
471
+ "name": target,
472
+ "file": path,
473
+ "line": lineno,
474
+ "text": stripped,
475
+ "is_definition": (path, lineno) in def_sites,
476
+ }
477
+ )
478
+
479
+ references.sort(key=lambda r: (r["file"], r["line"]))
480
+ total = len(references)
481
+ files = len({r["file"] for r in references})
482
+ notes = _reference_notes(target, references, total, files, file_contents)
483
+ return {
484
+ "name": target,
485
+ "total": total,
486
+ "files": files,
487
+ "references": references[:limit],
488
+ "notes": notes,
489
+ }
490
+
491
+
492
+ def _reference_notes(
493
+ target: str, references: list[dict], total: int, files: int, file_contents: dict[str, str]
494
+ ) -> list[str]:
495
+ scanned = sum(1 for p in file_contents if _lang(p) in ("python", "js"))
496
+ if scanned == 0:
497
+ return ["No Python or JS/TS files were found to search."]
498
+ if total == 0:
499
+ return [
500
+ f"'{target}' is not used in any Python or JS/TS file "
501
+ "(or it only appears inside strings or comments)."
502
+ ]
503
+
504
+ notes = [f"Found {total} reference(s) to '{target}' across {files} file(s)."]
505
+ def_count = sum(1 for r in references if r["is_definition"])
506
+ if def_count:
507
+ where = "is where" if def_count == 1 else "are where"
508
+ notes.append(f"{def_count} of these {where} '{target}' is defined; the rest are uses.")
509
+ notes.append(
510
+ "Whole-word text search: a use hidden inside a string or comment may be "
511
+ "missed, and a same-named field on an unrelated object may be counted."
512
+ )
513
+ return notes
514
+
515
+
516
+ # ---------------------------------------------------------------------------
517
+ # File outline — one file's structure, in the order it is written
518
+ # ---------------------------------------------------------------------------
519
+
520
+
521
+ def file_outline(file_contents: dict[str, str], path: str, *, limit: int = 1000) -> dict:
522
+ """Return one file's table of contents, in source order.
523
+
524
+ Where :func:`build_definition_index` flattens the whole project
525
+ alphabetically, this answers the question a reader has the moment they open
526
+ an unfamiliar file: "what is in here, top to bottom?". Top-level functions
527
+ and classes are listed in the order they appear, and a class carries its
528
+ methods nested underneath (also in source order), so the shape of the file
529
+ is visible before any of its detail.
530
+
531
+ Returns ``{"file", "lang", "total", "outline", "notes"}``. ``outline`` is a
532
+ list of entries (see :func:`_entry`) in line order; an entry that can hold
533
+ nested definitions — a class — gains a ``children`` list with its methods
534
+ (and any nested classes). ``total`` counts every definition, nested ones
535
+ included, before the ``limit`` truncation.
536
+ """
537
+ content = file_contents.get(path)
538
+ if content is None:
539
+ return {
540
+ "file": path,
541
+ "lang": _lang(path),
542
+ "total": 0,
543
+ "outline": [],
544
+ "notes": [f"'{path}' is not among the analyzed files."],
545
+ }
546
+
547
+ lang = _lang(path)
548
+ if lang == "python":
549
+ defs = _py_definitions(path, content)
550
+ elif lang == "js":
551
+ defs = _js_definitions(path, content)
552
+ else:
553
+ return {
554
+ "file": path,
555
+ "lang": lang,
556
+ "total": 0,
557
+ "outline": [],
558
+ "notes": ["The outline covers Python and JS/TS files; this file is neither."],
559
+ }
560
+
561
+ total = len(defs)
562
+ classes_by_name: dict[str, dict] = {}
563
+ outline: list[dict] = []
564
+ # Walk in source order so a class is always seen before its own members.
565
+ for d in sorted(defs, key=lambda e: e["line"]):
566
+ node = dict(d)
567
+ parent_node = classes_by_name.get(d["parent"]) if d["parent"] else None
568
+ if parent_node is not None:
569
+ parent_node["children"].append(node)
570
+ else:
571
+ # A method whose class is not itself indexed (an orphan) still gets
572
+ # surfaced at the top level rather than silently dropped.
573
+ outline.append(node)
574
+ if d["kind"] == "class":
575
+ node["children"] = []
576
+ classes_by_name[d["name"]] = node
577
+
578
+ notes = _outline_notes(path, lang, defs, total)
579
+ return {
580
+ "file": path,
581
+ "lang": lang,
582
+ "total": total,
583
+ "outline": outline[:limit],
584
+ "notes": notes,
585
+ }
586
+
587
+
588
+ def _outline_notes(path: str, lang: str, defs: list[dict], total: int) -> list[str]:
589
+ name = path.rsplit("/", 1)[-1]
590
+ if total == 0:
591
+ return [f"No top-level functions or classes were found in {name}."]
592
+
593
+ by_kind: dict[str, int] = {}
594
+ for d in defs:
595
+ by_kind[d["kind"]] = by_kind.get(d["kind"], 0) + 1
596
+ parts = [_count_phrase(by_kind[k], k) for k in sorted(by_kind)]
597
+ notes = [f"{name} defines {', '.join(parts)}, listed in the order they appear."]
598
+ if lang == "js":
599
+ notes.append(
600
+ "For JS/TS, class methods are broken out when the class body is "
601
+ "conventionally indented; arrow-function fields and computed names "
602
+ "are still listed under the class as a whole."
603
+ )
604
+ notes.append("Regex-based outline: runtime-built or re-exported names may be missed.")
605
+ return notes