sourcecode 0.38.0__py3-none-any.whl → 0.41.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.
sourcecode/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "0.38.0"
3
+ __version__ = "0.41.0"
sourcecode/cli.py CHANGED
@@ -220,6 +220,29 @@ def _preprocess_argv() -> None:
220
220
  _sys.argv = _sys.argv[:1] + modified
221
221
 
222
222
 
223
+ def _copy_to_clipboard(content: str) -> bool:
224
+ """Copy text to system clipboard. Returns True on success, False otherwise (never raises)."""
225
+ import subprocess
226
+ import sys as _sys
227
+ try:
228
+ if _sys.platform == "darwin":
229
+ subprocess.run(["pbcopy"], input=content.encode("utf-8"), check=True, timeout=10)
230
+ return True
231
+ elif _sys.platform == "win32":
232
+ subprocess.run(["clip"], input=content.encode("utf-16"), check=True, timeout=10)
233
+ return True
234
+ else:
235
+ for cmd in (["xclip", "-selection", "clipboard"], ["xsel", "--clipboard", "--input"]):
236
+ try:
237
+ subprocess.run(cmd, input=content.encode("utf-8"), check=True, timeout=10)
238
+ return True
239
+ except (FileNotFoundError, subprocess.CalledProcessError):
240
+ continue
241
+ return False
242
+ except Exception:
243
+ return False
244
+
245
+
223
246
  app = typer.Typer(
224
247
  name="sourcecode",
225
248
  help=_HELP,
@@ -543,7 +566,7 @@ def main(
543
566
  entrypoints_only: bool = typer.Option(
544
567
  False,
545
568
  "--entrypoints-only",
546
- help="Contract mode: include only files that are entrypoints or have exported symbols.",
569
+ help="Contract mode: include only files that are runtime entrypoints or have exported symbols (public API surface). Note: 'entrypoints' here includes all files with exports, not strictly detected runtime entry points.",
547
570
  ),
548
571
  changed_only: bool = typer.Option(
549
572
  False,
@@ -571,6 +594,12 @@ def main(
571
594
  "--symbol",
572
595
  help="Contract mode: extract localized context for a specific symbol name. Returns defining file + all importers.",
573
596
  ),
597
+ copy: bool = typer.Option(
598
+ False,
599
+ "--copy",
600
+ "-c",
601
+ help="Copy output to system clipboard after a successful run. No-op when --output is used or clipboard is unavailable.",
602
+ ),
574
603
  ) -> None:
575
604
  """Analyze a repository and produce structured context for AI coding agents.
576
605
 
@@ -1127,11 +1156,15 @@ def main(
1127
1156
 
1128
1157
  _all_call_files = set(_fan_in) | set(_fan_out)
1129
1158
  _hotspots: list[dict] = []
1130
- # Filter test paths from hotspots — they dominate fan-in by calling many modules
1159
+ # Filter test, noise, and auxiliary paths — they dominate fan-in but carry no signal
1131
1160
  _TEST_MARKERS = {"/test", "/tests", "/spec", "/specs", "_test.", ".test.", ".spec."}
1161
+ from sourcecode.ranking_engine import RankingEngine as _RankingEngine
1162
+ _sem_engine = _RankingEngine(sm.monorepo_packages)
1132
1163
  for _p in _all_call_files:
1133
1164
  if any(_m in _p for _m in _TEST_MARKERS) or _p.startswith("test"):
1134
1165
  continue
1166
+ if _sem_engine.is_noise(_p) or _sem_engine.is_auxiliary(_p):
1167
+ continue
1135
1168
  _in = _fan_in[_p]
1136
1169
  _out = _fan_out[_p]
1137
1170
  _score = _in * 2.0 + _out * 1.0
@@ -1386,6 +1419,13 @@ def main(
1386
1419
  # 6. Write output (CLI-04)
1387
1420
  write_output(content, output=output)
1388
1421
 
1422
+ # 7. Clipboard copy (--copy / -c)
1423
+ if copy and output is None:
1424
+ _trimmed = content.strip()
1425
+ if _trimmed and _trimmed not in ("{}", "[]", "null"):
1426
+ if _copy_to_clipboard(content):
1427
+ typer.echo("✓ copied to clipboard", err=True)
1428
+
1389
1429
 
1390
1430
  @app.command("prepare-context")
1391
1431
  def prepare_context_cmd(
@@ -1417,6 +1457,12 @@ def prepare_context_cmd(
1417
1457
  "--dry-run",
1418
1458
  help="Show what would be analyzed without running it",
1419
1459
  ),
1460
+ copy: bool = typer.Option(
1461
+ False,
1462
+ "--copy",
1463
+ "-c",
1464
+ help="Copy output to system clipboard after a successful run. No-op when clipboard is unavailable.",
1465
+ ),
1420
1466
  ) -> None:
1421
1467
  """Task-specific context for AI coding agents.
1422
1468
 
@@ -1514,7 +1560,14 @@ def prepare_context_cmd(
1514
1560
  if llm_prompt:
1515
1561
  out["llm_prompt"] = builder.render_prompt(output)
1516
1562
 
1517
- typer.echo(json.dumps(out, indent=2, ensure_ascii=False))
1563
+ _pc_content = json.dumps(out, indent=2, ensure_ascii=False)
1564
+ typer.echo(_pc_content)
1565
+
1566
+ if copy:
1567
+ _trimmed = _pc_content.strip()
1568
+ if _trimmed and _trimmed not in ("{}", "[]", "null"):
1569
+ if _copy_to_clipboard(_pc_content):
1570
+ typer.echo("✓ copied to clipboard", err=True)
1518
1571
 
1519
1572
 
1520
1573
  # ── Telemetry commands ────────────────────────────────────────────────────────
@@ -91,6 +91,7 @@ class FileContract:
91
91
  fan_out: int = 0 # how many files this imports
92
92
  is_entrypoint: bool = False
93
93
  is_changed: bool = False
94
+ ranking_reasons: list[str] = field(default_factory=list)
94
95
 
95
96
  # Extraction quality
96
97
  extraction_method: str = "heuristic" # ast | tree_sitter | heuristic
@@ -17,6 +17,7 @@ from typing import Any, Literal, Optional
17
17
 
18
18
  from sourcecode.ast_extractor import AstExtractor, _LANGUAGE_MAP
19
19
  from sourcecode.contract_model import ContractSummary, FileContract
20
+ from sourcecode.ranking_engine import RankingEngine
20
21
  from sourcecode.relevance_scorer import RelevanceScorer
21
22
  from sourcecode.schema import EntryPoint, MonorepoPackageInfo
22
23
 
@@ -27,22 +28,6 @@ from sourcecode.schema import EntryPoint, MonorepoPackageInfo
27
28
  _MAX_FILES = 500 # hard cap on files extracted per run
28
29
  _SRC_EXTENSIONS: frozenset[str] = frozenset(_LANGUAGE_MAP.keys())
29
30
 
30
- # Role-based score adjustments applied after contract extraction.
31
- # Runtime roles get a boost; config/util are neutral or penalized.
32
- _ROLE_SCORE: dict[str, float] = {
33
- "entrypoint": 0.15,
34
- "service": 0.10,
35
- "route": 0.10,
36
- "api": 0.08,
37
- "middleware": 0.06,
38
- "store": 0.05,
39
- "model": 0.05,
40
- "hook": 0.05,
41
- "component": 0.03,
42
- "util": 0.00,
43
- "config": -0.10,
44
- "unknown": 0.00,
45
- }
46
31
 
47
32
  RankStrategy = Literal["relevance", "centrality", "git-churn"]
48
33
 
@@ -194,6 +179,7 @@ class ContractPipeline:
194
179
  """
195
180
  entry_paths = {ep.path.replace("\\", "/") for ep in (entry_points or [])}
196
181
  scorer = RelevanceScorer(monorepo_packages)
182
+ engine = RankingEngine(monorepo_packages)
197
183
 
198
184
  # 1. Changed files (for --changed-only and ranking)
199
185
  changed_files: set[str] = set()
@@ -267,9 +253,24 @@ class ContractPipeline:
267
253
  if rank_by == "git-churn":
268
254
  churn = _get_git_churn(root, [c.path for c in contracts])
269
255
 
270
- # 6. Compute relevance scores
256
+ # 6. Compute relevance scores via unified ranking engine
257
+ max_fan_in = max((c.fan_in for c in contracts), default=1) if contracts else 1
258
+ max_churn_val = max(churn.values(), default=1) if churn else 1
271
259
  for c in contracts:
272
- c.relevance_score = self._score(c, scorer, churn)
260
+ fs = engine.score(
261
+ c.path,
262
+ fan_in=c.fan_in,
263
+ fan_out=c.fan_out,
264
+ max_fan_in=max_fan_in,
265
+ git_churn=churn.get(c.path, 0),
266
+ max_churn=max_churn_val,
267
+ is_entrypoint=c.is_entrypoint,
268
+ is_changed=c.is_changed,
269
+ export_count=len(c.exports),
270
+ task="default",
271
+ )
272
+ c.relevance_score = fs.display_score
273
+ c.ranking_reasons = fs.reasons
273
274
 
274
275
  # 7. Rank
275
276
  contracts = self._rank(contracts, rank_by)
@@ -285,7 +286,7 @@ class ContractPipeline:
285
286
  known_paths=set(src_paths),
286
287
  entry_paths=entry_paths,
287
288
  changed_files=changed_files,
288
- scorer=scorer,
289
+ engine=engine,
289
290
  )
290
291
 
291
292
  # 9. Entrypoints-only filter
@@ -312,45 +313,13 @@ class ContractPipeline:
312
313
  )
313
314
  return contracts, summary
314
315
 
315
- def _score(
316
- self,
317
- c: FileContract,
318
- scorer: RelevanceScorer,
319
- churn: dict[str, int],
320
- ) -> float:
321
- base = scorer.score(c.path)
322
-
323
- if c.is_entrypoint:
324
- base += 0.3
325
- if c.is_changed:
326
- base += 0.2
327
-
328
- # Fan-in is the strongest signal: many callers = critical contract
329
- fi_score = min(c.fan_in / 10.0, 0.3)
330
- fo_score = min(c.fan_out / 15.0, 0.15)
331
- base += fi_score + fo_score
332
-
333
- # Exported API value
334
- export_count = len(c.exports)
335
- base += min(export_count / 20.0, 0.1)
336
-
337
- # Churn
338
- churn_score = min(churn.get(c.path, 0) / 20.0, 0.1)
339
- base += churn_score
340
-
341
- # Role-based boost: runtime roles score higher than auxiliary
342
- base += _ROLE_SCORE.get(c.role, 0.0)
343
-
344
- return min(1.0, base)
345
-
346
316
  def _rank(self, contracts: list[FileContract], rank_by: RankStrategy) -> list[FileContract]:
347
317
  if rank_by == "centrality":
348
- # Approximate centrality: fan_in + fan_out
349
- return sorted(contracts, key=lambda c: -(c.fan_in + c.fan_out))
318
+ return sorted(contracts, key=lambda c: (-(c.fan_in + c.fan_out), c.path))
350
319
  if rank_by == "git-churn":
351
- return sorted(contracts, key=lambda c: (-c.is_changed, -c.relevance_score))
352
- # Default: relevance
353
- return sorted(contracts, key=lambda c: (-c.is_entrypoint, -c.relevance_score))
320
+ return sorted(contracts, key=lambda c: (-c.is_changed, -c.relevance_score, c.path))
321
+ # Default: relevance — path breaks ties deterministically
322
+ return sorted(contracts, key=lambda c: (-c.is_entrypoint, -c.relevance_score, c.path))
354
323
 
355
324
  def _symbol_deep_scan(
356
325
  self,
@@ -359,7 +328,7 @@ class ContractPipeline:
359
328
  known_paths: set[str],
360
329
  entry_paths: set[str],
361
330
  changed_files: set[str],
362
- scorer: RelevanceScorer,
331
+ engine: RankingEngine,
363
332
  ) -> list[FileContract]:
364
333
  """Grep-based fallback when the shallow scan missed the defining files.
365
334
 
@@ -367,7 +336,7 @@ class ContractPipeline:
367
336
  extracts contracts for candidates not already processed, then re-applies
368
337
  the symbol filter. Fan-in/fan-out are not computed for these contracts.
369
338
  """
370
- candidates = _find_symbol_files(root, symbol, known_paths, scorer)
339
+ candidates = _find_symbol_files(root, symbol, known_paths, engine)
371
340
  if not candidates:
372
341
  return []
373
342
 
@@ -379,7 +348,9 @@ class ContractPipeline:
379
348
  continue
380
349
  contract.is_entrypoint = rel_path in entry_paths
381
350
  contract.is_changed = rel_path in changed_files
382
- contract.relevance_score = scorer.score(rel_path)
351
+ fs = engine.score(rel_path, is_entrypoint=contract.is_entrypoint, is_changed=contract.is_changed)
352
+ contract.relevance_score = fs.display_score
353
+ contract.ranking_reasons = fs.reasons
383
354
  extra.append(contract)
384
355
 
385
356
  return _filter_by_symbol(extra, symbol)
@@ -531,7 +502,7 @@ def _find_symbol_files(
531
502
  root: Path,
532
503
  symbol: str,
533
504
  known_paths: set[str],
534
- scorer: RelevanceScorer,
505
+ engine: RankingEngine,
535
506
  ) -> list[str]:
536
507
  """Find source files outside *known_paths* that contain *symbol* as text.
537
508
 
@@ -560,7 +531,7 @@ def _find_symbol_files(
560
531
  if line.startswith("./"):
561
532
  line = line[2:]
562
533
  line = line.replace("\\", "/")
563
- if line and line not in known_paths and not scorer.is_noise(line):
534
+ if line and line not in known_paths and not engine.is_noise(line):
564
535
  found.append(line)
565
536
  return found
566
537
  except Exception:
@@ -578,7 +549,7 @@ def _find_symbol_files(
578
549
  rel_str = str(rel).replace("\\", "/")
579
550
  except ValueError:
580
551
  continue
581
- if rel_str in known_paths or scorer.is_noise(rel_str):
552
+ if rel_str in known_paths or engine.is_noise(rel_str):
582
553
  continue
583
554
  try:
584
555
  content = Path(full).read_text(encoding="utf-8", errors="replace")
@@ -185,6 +185,13 @@ class DocAnalyzer:
185
185
  if any(r.doc_text and r.doc_text.endswith(self._TRUNCATION_SUFFIX) for r in records):
186
186
  truncated = True
187
187
 
188
+ # Explicit absence signal: scanned files but found nothing
189
+ if total_count == 0 and file_paths:
190
+ limitations.append(
191
+ f"no_docs_found: {len(file_paths)} file(s) scanned, "
192
+ "no docstrings or JSDoc comments found"
193
+ )
194
+
188
195
  summary = DocSummary(
189
196
  requested=True,
190
197
  total_count=total_count,
@@ -20,12 +20,13 @@ _RELEASE_COMMIT_RE = re.compile(
20
20
  )
21
21
  # Matches version-bump phrases anywhere in the commit subject (multilingual)
22
22
  _RELEASE_COMMIT_CONTAINS_RE = re.compile(
23
- r"subiendo a v?[\d.]" # Spanish: "subiendo a v.0.28.0"
23
+ r"subiendo a v?[\d.]" # Spanish: "subiendo a 0.38.0", "subiendo a v.0.31.0"
24
+ r"|actualizando a v?[\d.]" # Spanish: "actualizando a 0.15.1"
24
25
  r"|bumping to v?[\d.]"
25
26
  r"|preparing (?:v|release)[\d. ]"
26
27
  r"|releasing v?[\d.]"
27
28
  r"|cut v?[\d.]"
28
- r"|\bv\d+\.\d+\.\d+\b", # bare version tag in middle of message
29
+ r"|\bv\d+\.\d+\.\d+\b", # bare version tag in middle of message
29
30
  re.IGNORECASE,
30
31
  )
31
32
 
@@ -34,12 +35,25 @@ _HOTSPOT_ADMIN_FILENAMES: frozenset[str] = frozenset({
34
35
  "CHANGELOG.md", "CHANGELOG", "CHANGES.md", "CHANGES", "HISTORY.md",
35
36
  "RELEASE.md", "RELEASES.md", "RELEASE_NOTES.md", "CHANGELOG.rst", "NEWS.md", "NEWS.rst",
36
37
  "VERSION", "VERSION.txt", "version.txt", ".version",
38
+ "_version.py", "__version__.py", "version.py",
39
+ "pyproject.toml", "setup.cfg",
37
40
  "package-lock.json", "yarn.lock", "pnpm-lock.yaml", "bun.lockb",
38
41
  "Cargo.lock", "poetry.lock", "Pipfile.lock", "composer.lock",
39
42
  "go.sum", "Gemfile.lock",
40
43
  })
41
44
  _HOTSPOT_ADMIN_SUFFIXES: tuple[str, ...] = (".lock", ".snap", ".min.js", ".min.css")
42
45
 
46
+ # Auxiliary directory names whose files should be excluded from hotspots —
47
+ # docs, examples, benchmarks etc. are high-commit but low operational signal.
48
+ _HOTSPOT_AUX_DIRS: frozenset[str] = frozenset({
49
+ "docs", "doc", "benchmark", "benchmarks", "example", "examples",
50
+ "demo", "demos", "playground", "playgrounds", "fixture", "fixtures",
51
+ "generated", "generate", "storybook", ".storybook", "stories",
52
+ "sandbox", "sandboxes",
53
+ "ci", "translations", "locales", "locale", "i18n", "l10n",
54
+ ".planning",
55
+ })
56
+
43
57
 
44
58
  def _run_git(args: list[str], cwd: Path, timeout: int = 15) -> tuple[str, int]:
45
59
  result = subprocess.run(
@@ -191,7 +205,7 @@ def _parse_commits(output: str) -> list:
191
205
 
192
206
 
193
207
  def _is_hotspot_admin(path: str) -> bool:
194
- """True for files that are noisy from release/bot commits, not semantic changes."""
208
+ """True for files that are noisy from release/bot commits or auxiliary dirs."""
195
209
  filename = path.rsplit("/", 1)[-1]
196
210
  if filename in _HOTSPOT_ADMIN_FILENAMES:
197
211
  return True
@@ -202,9 +216,15 @@ def _is_hotspot_admin(path: str) -> bool:
202
216
  _lower = filename.lower()
203
217
  if _lower.startswith("changelog.") or _lower.startswith("changes."):
204
218
  return True
205
- # lerna.json and root-level package.json are modified by version bumps, not dev work
219
+ # lerna.json is modified by version bumps, not dev work
206
220
  if filename in ("lerna.json",):
207
221
  return True
222
+ # Auxiliary directory parts — docs, benchmarks, examples, demos, etc.
223
+ # These may have high commit counts but are not operational signal for agents.
224
+ parts = path.split("/")
225
+ for part in parts[:-1]: # check directory components, not the filename itself
226
+ if part.lower() in _HOTSPOT_AUX_DIRS:
227
+ return True
208
228
  return False
209
229
 
210
230
 
@@ -231,6 +251,9 @@ def _parse_hotspots(output: str) -> list:
231
251
  continue
232
252
  if skip_commit:
233
253
  continue
254
+ # Skip git artifact lines that are not file paths: flags (-o, --), separators, etc.
255
+ if line.startswith("-") or not ("/" in line or "." in line):
256
+ continue
234
257
  if _is_hotspot_admin(line):
235
258
  continue
236
259
  file_counts[line] += 1
@@ -627,94 +627,81 @@ class TaskContextBuilder:
627
627
  git_hotspots: Optional[dict[str, int]] = None,
628
628
  uncommitted_files: Optional[set[str]] = None,
629
629
  ) -> list[RelevantFile]:
630
- from sourcecode.relevance_scorer import RelevanceScorer
630
+ from sourcecode.ranking_engine import RankingEngine
631
631
  from sourcecode.file_classifier import FileClassifier
632
- scorer = RelevanceScorer(monorepo_packages or [])
633
- file_classifier = FileClassifier(self.root, [
634
- # _rank_files only needs production path evidence; EntryPoint objects
635
- # are not available here, so category evidence is best-effort below.
636
- ], monorepo_packages or [])
637
632
 
638
- # Auxiliary entry points (benchmark, docs, examples) must not get
639
- # the production entry boost they are not runtime signals.
640
- runtime_entry_set = {ep for ep in entry_set if not scorer.is_auxiliary(ep)}
633
+ engine = RankingEngine(monorepo_packages or [])
634
+ file_classifier = FileClassifier(self.root, [], monorepo_packages or [])
635
+
636
+ # Auxiliary entry points (benchmark, docs, examples) are not runtime
637
+ runtime_entry_set = {ep for ep in entry_set if not engine.is_auxiliary(ep)}
641
638
 
642
639
  _hotspots = git_hotspots or {}
643
640
  _uncommitted = uncommitted_files or set()
644
641
  _max_churn = max(_hotspots.values(), default=1)
645
642
 
646
- scored: list[tuple[float, RelevantFile]] = []
643
+ scored: list[tuple[float, str, RelevantFile]] = []
647
644
 
648
645
  for path in all_paths:
649
646
  if Path(path).suffix.lower() not in _ALL_EXTENSIONS:
650
647
  continue
651
648
  if any(pen in path for pen in spec.ranking_penalties):
652
649
  continue
653
-
654
- # Hard filter: tooling/config noise
655
- if scorer.is_noise(path):
650
+ if engine.is_noise(path):
656
651
  continue
657
652
 
658
653
  is_test = path in test_set
659
654
  if is_test and task_name != "generate-tests":
660
655
  continue
661
656
 
662
- score = 0.0
663
- reasons: list[str] = []
657
+ # Structural + git signals from unified engine (task-weighted)
658
+ fs = engine.score(
659
+ path,
660
+ is_entrypoint=(path in runtime_entry_set),
661
+ git_churn=_hotspots.get(path, 0),
662
+ max_churn=_max_churn,
663
+ is_changed=(path in _uncommitted),
664
+ task=task_name,
665
+ )
664
666
 
665
- # Only runtime entry points get the production boost
666
- if path in runtime_entry_set:
667
- score += 3.0
668
- reasons.append("entry point")
667
+ if fs.score < -50: # hard noise
668
+ continue
669
669
 
670
+ # Content classification boost (reads file imports)
671
+ content_boost = 0.0
672
+ content_reasons: list[str] = []
670
673
  file_class = file_classifier.classify(path)
671
674
  if file_class is not None:
672
- score += file_class.relevance * 2.0
673
- reasons.append(f"{file_class.category}: {file_class.reason}")
675
+ content_boost = file_class.relevance * 2.0
676
+ content_reasons.append(f"{file_class.category}: {file_class.reason}")
674
677
 
675
678
  if is_test:
676
- score += 2.0
677
- reasons.append("existing test")
678
- elif self._is_source(path):
679
- score += 0.5
680
- if not reasons:
681
- reasons.append("source file with supported extension")
682
-
683
- # Operational relevance boost/penalty from package role
684
- rel = scorer.score(path)
685
- score += (rel - 0.3) * 2.0 # center around 0.3 baseline
686
-
687
- # Suppress auxiliary dirs (benchmarks, docs, examples, demos)
688
- if scorer.is_auxiliary(path):
689
- score -= 2.0
690
-
691
- # Git churn: frequently changed files are high-signal for active work
692
- churn = _hotspots.get(path, 0)
693
- if churn > 0:
694
- score += (churn / _max_churn) * 1.5
695
- reasons.append(f"git churn ({churn})")
696
-
697
- # Uncommitted changes: files actively being edited rank highest
698
- if path in _uncommitted:
699
- score += 1.0
700
- reasons.append("uncommitted changes")
701
-
702
- if score <= 0:
679
+ content_boost += 2.0
680
+ content_reasons.append("existing test")
681
+ elif self._is_source(path) and not content_reasons:
682
+ content_boost += 0.5
683
+
684
+ total = fs.score + content_boost
685
+ if total <= 0:
703
686
  continue
704
687
 
705
688
  role = (
706
689
  "entrypoint" if path in runtime_entry_set
707
690
  else ("test" if is_test else "source")
708
691
  )
709
- scored.append((score, RelevantFile(
692
+ all_reasons = [r for r in fs.reasons if r != "source file"] + content_reasons
693
+ reason_str = ", ".join(all_reasons) if all_reasons else "source file"
694
+
695
+ scored.append((total, path, RelevantFile(
710
696
  path=path,
711
697
  role=role,
712
- score=round(score, 1),
713
- reason=", ".join(reasons) if reasons else "source file",
698
+ score=round(min(total / 3.0, 1.0), 2),
699
+ reason=reason_str,
714
700
  )))
715
701
 
716
- scored.sort(key=lambda x: -x[0])
717
- return [f for _, f in scored[:15]]
702
+ # Deterministic: score desc, then path asc as tiebreaker
703
+ scored.sort(key=lambda x: (-x[0], x[1]))
704
+ return [f for _, _, f in scored[:15]]
718
705
 
719
706
  def _is_test(self, path: str) -> bool:
720
707
  name = Path(path).name.lower()
@@ -0,0 +1,231 @@
1
+ from __future__ import annotations
2
+
3
+ """Unified, deterministic file ranking engine.
4
+
5
+ Single source of truth for scoring files by AI-agent relevance.
6
+ Used by: contract_pipeline, prepare_context, serializer (_file_relevance).
7
+
8
+ Score components (all weighted by task profile):
9
+ path_relevance — structural path signals (source roots, entrypoint stems, noise)
10
+ entrypoint — runtime entry point boost
11
+ fan_in — import centrality: how many other files import this
12
+ fan_out — hub signal: how many files this imports
13
+ git_churn — recent commit frequency
14
+ code_notes — bug/fixme annotation density
15
+ exports — public API surface size
16
+ is_changed — uncommitted or recently modified
17
+
18
+ Determinism: callers should sort by (-score, path). Path breaks all ties
19
+ reproducibly so re-runs on an unchanged repo produce identical rankings.
20
+ """
21
+
22
+ from dataclasses import dataclass
23
+ from typing import Optional
24
+
25
+ from sourcecode.relevance_scorer import RelevanceScorer
26
+ from sourcecode.schema import MonorepoPackageInfo
27
+
28
+
29
+ @dataclass
30
+ class FileScore:
31
+ path: str
32
+ score: float # raw ranking score (higher = more relevant)
33
+ display_score: float # 0.0–1.0 for backward-compat output fields
34
+ reasons: list[str] # human-readable signal labels
35
+
36
+
37
+ @dataclass
38
+ class TaskWeights:
39
+ """Per-signal weights for a specific task profile."""
40
+ path_relevance: float = 1.0
41
+ entrypoint: float = 1.0
42
+ fan_in: float = 1.0
43
+ fan_out: float = 0.5
44
+ git_churn: float = 0.5
45
+ code_notes: float = 0.5
46
+ exports: float = 0.3
47
+ is_changed: float = 0.8
48
+
49
+
50
+ # Task profiles: each emphasizes different signals for different agent goals.
51
+ # The contrast between profiles is intentional — fix-bug and explain must
52
+ # produce meaningfully different ranked sets from the same codebase.
53
+ TASK_WEIGHTS: dict[str, TaskWeights] = {
54
+ # fix-bug: files with bug annotations, recent churn, actively changed logic
55
+ "fix-bug": TaskWeights(
56
+ path_relevance=0.5, entrypoint=0.5,
57
+ fan_in=0.8, fan_out=0.3,
58
+ git_churn=1.5, code_notes=3.0,
59
+ exports=0.2, is_changed=2.0,
60
+ ),
61
+ # refactor: highly-coupled files, technical debt, complex hubs
62
+ "refactor": TaskWeights(
63
+ path_relevance=0.8, entrypoint=0.3,
64
+ fan_in=2.0, fan_out=2.0,
65
+ git_churn=0.3, code_notes=2.0,
66
+ exports=1.0, is_changed=0.3,
67
+ ),
68
+ # explain: stable core, entrypoints, central modules — ignore churn noise
69
+ "explain": TaskWeights(
70
+ path_relevance=2.0, entrypoint=3.0,
71
+ fan_in=0.8, fan_out=0.3,
72
+ git_churn=0.0, code_notes=0.0,
73
+ exports=0.5, is_changed=0.0,
74
+ ),
75
+ # onboard: same as explain but also values hub modules
76
+ "onboard": TaskWeights(
77
+ path_relevance=2.0, entrypoint=3.0,
78
+ fan_in=1.2, fan_out=0.5,
79
+ git_churn=0.0, code_notes=0.0,
80
+ exports=1.0, is_changed=0.0,
81
+ ),
82
+ # generate-tests: source files with large public API, not yet covered
83
+ "generate-tests": TaskWeights(
84
+ path_relevance=0.8, entrypoint=0.3,
85
+ fan_in=1.5, fan_out=0.8,
86
+ git_churn=0.5, code_notes=0.5,
87
+ exports=2.5, is_changed=0.5,
88
+ ),
89
+ # review-pr: changed files and their importers
90
+ "review-pr": TaskWeights(
91
+ path_relevance=0.5, entrypoint=0.5,
92
+ fan_in=1.5, fan_out=0.5,
93
+ git_churn=0.5, code_notes=0.8,
94
+ exports=0.3, is_changed=3.0,
95
+ ),
96
+ # delta: changed files and dependency impact
97
+ "delta": TaskWeights(
98
+ path_relevance=0.5, entrypoint=0.5,
99
+ fan_in=1.5, fan_out=0.5,
100
+ git_churn=0.5, code_notes=0.5,
101
+ exports=0.3, is_changed=3.0,
102
+ ),
103
+ # default: balanced, no task bias
104
+ "default": TaskWeights(),
105
+ }
106
+
107
+ _WORKSPACE_CORE_ROLES = frozenset({
108
+ "runtime_core", "backend_runtime", "frontend_runtime", "plugin_host",
109
+ "composition_layer",
110
+ })
111
+ _WORKSPACE_NOISE_ROLES = frozenset({
112
+ "benchmark_layer", "tooling_layer", "docs_layer", "test_layer",
113
+ })
114
+
115
+
116
+ class RankingEngine:
117
+ """Unified file ranking engine.
118
+
119
+ Stateless once constructed. Create one instance per analysis run.
120
+ """
121
+
122
+ def __init__(
123
+ self,
124
+ monorepo_packages: Optional[list[MonorepoPackageInfo]] = None,
125
+ ) -> None:
126
+ self._scorer = RelevanceScorer(monorepo_packages or [])
127
+
128
+ def score(
129
+ self,
130
+ path: str,
131
+ *,
132
+ fan_in: int = 0,
133
+ fan_out: int = 0,
134
+ max_fan_in: int = 10,
135
+ git_churn: int = 0,
136
+ max_churn: int = 10,
137
+ is_entrypoint: bool = False,
138
+ is_changed: bool = False,
139
+ code_note_count: int = 0,
140
+ export_count: int = 0,
141
+ task: str = "default",
142
+ ) -> FileScore:
143
+ """Compute a scored, explained ranking for a single file.
144
+
145
+ Returns FileScore with:
146
+ score — raw float for ranking comparisons
147
+ display_score — clamped 0.0–1.0 for output fields
148
+ reasons — list of human-readable signal labels
149
+ """
150
+ norm = path.replace("\\", "/").lstrip("/")
151
+
152
+ if self._scorer.is_noise(norm):
153
+ return FileScore(path=path, score=-100.0, display_score=0.0, reasons=["noise"])
154
+
155
+ w = TASK_WEIGHTS.get(task, TASK_WEIGHTS["default"])
156
+ reasons: list[str] = []
157
+ raw = 0.0
158
+
159
+ # 1. Structural path relevance (0.0–1.0 from RelevanceScorer)
160
+ path_rel = self._scorer.score(norm)
161
+ raw += path_rel * w.path_relevance
162
+
163
+ # 2. Runtime entrypoint
164
+ if is_entrypoint:
165
+ raw += 0.3 * w.entrypoint
166
+ reasons.append("runtime entrypoint")
167
+
168
+ # 3. Fan-in: import centrality
169
+ if fan_in > 0 and w.fan_in > 0:
170
+ fi_norm = min(fan_in / max(max_fan_in, 1), 1.0)
171
+ raw += fi_norm * 0.3 * w.fan_in
172
+ if fan_in >= 5:
173
+ reasons.append(f"high import centrality (fan_in={fan_in})")
174
+ elif fan_in >= 2:
175
+ reasons.append(f"imported by {fan_in} modules")
176
+ else:
177
+ reasons.append(f"imported by {fan_in} module")
178
+
179
+ # 4. Fan-out: hub signal (only when significant to avoid false positives)
180
+ if fan_out >= 5 and w.fan_out > 0:
181
+ fo_norm = min(fan_out / 20.0, 1.0)
182
+ raw += fo_norm * 0.15 * w.fan_out
183
+ reasons.append(f"hub module (fan_out={fan_out})")
184
+
185
+ # 5. Git churn: recently active files are high-signal for fix/review tasks
186
+ if git_churn > 0 and w.git_churn > 0:
187
+ churn_norm = min(git_churn / max(max_churn, 1), 1.0)
188
+ raw += churn_norm * 0.2 * w.git_churn
189
+ reasons.append(f"recent churn ({git_churn} commits)")
190
+
191
+ # 6. Code annotation density (TODO/FIXME/BUG/HACK)
192
+ if code_note_count > 0 and w.code_notes > 0:
193
+ notes_norm = min(code_note_count / 10.0, 1.0)
194
+ raw += notes_norm * 0.15 * w.code_notes
195
+ reasons.append(f"bug-note density ({code_note_count} annotations)")
196
+
197
+ # 7. Export surface size
198
+ if export_count > 0 and w.exports > 0:
199
+ raw += min(export_count / 20.0, 0.1) * w.exports
200
+
201
+ # 8. Uncommitted or recently changed
202
+ if is_changed and w.is_changed > 0:
203
+ raw += 0.2 * w.is_changed
204
+ reasons.append("uncommitted changes")
205
+
206
+ # Monorepo package role
207
+ pkg_role = self._scorer.package_role(norm)
208
+ if pkg_role in _WORKSPACE_CORE_ROLES:
209
+ reasons.append("workspace source root")
210
+ elif pkg_role in _WORKSPACE_NOISE_ROLES:
211
+ raw -= 0.3
212
+
213
+ # Auxiliary dir hard penalty (docs, benchmarks, examples, demos)
214
+ if self._scorer.is_auxiliary(norm):
215
+ raw -= 2.0
216
+
217
+ if not reasons:
218
+ reasons.append("source file")
219
+
220
+ display = max(0.0, min(1.0, raw))
221
+ return FileScore(path=path, score=raw, display_score=display, reasons=reasons)
222
+
223
+ def rank(self, scores: list[FileScore]) -> list[FileScore]:
224
+ """Deterministic sort: highest score first, path breaks all ties."""
225
+ return sorted(scores, key=lambda s: (-s.score, s.path))
226
+
227
+ def is_noise(self, path: str) -> bool:
228
+ return self._scorer.is_noise(path)
229
+
230
+ def is_auxiliary(self, path: str) -> bool:
231
+ return self._scorer.is_auxiliary(path)
@@ -222,6 +222,10 @@ class RelevanceScorer:
222
222
  def _is_auxiliary(self, norm: str) -> bool:
223
223
  return any(p.search(norm) for p in _AUXILIARY_DIR_PATTERNS)
224
224
 
225
+ def package_role(self, path: str) -> str:
226
+ """Return the monorepo package role for this path, or empty string."""
227
+ return self._package_role(path.replace("\\", "/").lstrip("/"))
228
+
225
229
  def _package_role(self, norm: str) -> str:
226
230
  for prefix, role in self._pkg_roles.items():
227
231
  if norm.startswith(prefix):
@@ -105,9 +105,13 @@ class SemanticAnalyzer:
105
105
 
106
106
  # 1. Flatten file_tree and filter to Python files
107
107
  all_paths = flatten_file_tree(file_tree)
108
+ from sourcecode.relevance_scorer import RelevanceScorer as _RS
109
+ _is_noise = _RS([]).is_noise
108
110
  source_files = [
109
111
  p for p in all_paths
110
- if Path(p).suffix in _PY_EXTENSIONS and (root / p).is_file()
112
+ if Path(p).suffix in _PY_EXTENSIONS
113
+ and (root / p).is_file()
114
+ and not _is_noise(p)
111
115
  ]
112
116
 
113
117
  # Files referenced in tree but not on disk (read_error)
@@ -347,7 +351,9 @@ class SemanticAnalyzer:
347
351
  # -----------------------------------------------------------------------
348
352
  js_source_files = [
349
353
  p for p in all_paths
350
- if Path(p).suffix in self._NODE_EXTENSIONS and (root / p).is_file()
354
+ if Path(p).suffix in self._NODE_EXTENSIONS
355
+ and (root / p).is_file()
356
+ and not _is_noise(p)
351
357
  ]
352
358
  internal_module_paths: set[str] = set(js_source_files)
353
359
 
sourcecode/serializer.py CHANGED
@@ -171,34 +171,91 @@ def _dep_import_key(name: str) -> str:
171
171
 
172
172
 
173
173
  def _file_relevance(sm: SourceMap, *, limit: int = 15) -> list[dict[str, Any]]:
174
+ from sourcecode.ranking_engine import RankingEngine
175
+
174
176
  root = Path(sm.metadata.analyzed_path) if sm.metadata.analyzed_path else Path(".")
175
177
  classifier = FileClassifier(root, sm.entry_points, sm.monorepo_packages)
176
- items = classifier.classify_paths(sm.file_paths, limit=limit)
177
- return [asdict(item) for item in items]
178
+ engine = RankingEngine(sm.monorepo_packages)
179
+
180
+ # Incorporate git hotspots when --git-context was passed
181
+ git_churn: dict[str, int] = {}
182
+ gc = sm.git_context
183
+ if (gc and gc.requested and gc.change_hotspots
184
+ and not any(lim in gc.limitations
185
+ for lim in ("no_git_repo", "git_not_found", "git_timeout"))):
186
+ git_churn = {h.file: h.commit_count for h in gc.change_hotspots}
187
+ max_churn = max(git_churn.values(), default=1)
188
+
189
+ entry_paths = {ep.path for ep in sm.entry_points}
190
+ scored: list[tuple[float, dict[str, Any]]] = []
191
+
192
+ for path in sm.file_paths:
193
+ file_class = classifier.classify(path)
194
+ fs = engine.score(
195
+ path,
196
+ git_churn=git_churn.get(path, 0),
197
+ max_churn=max_churn,
198
+ is_entrypoint=path in entry_paths,
199
+ )
200
+
201
+ if fs.score < -50: # hard noise
202
+ continue
203
+
204
+ content_rel = file_class.relevance if file_class else 0.0
205
+ combined = fs.score + content_rel
206
+
207
+ if combined <= 0 and not (file_class and file_class.relevance > 0.3):
208
+ continue
209
+
210
+ item: dict[str, Any] = {
211
+ "path": path,
212
+ "category": file_class.category if file_class else "source",
213
+ "confidence": file_class.confidence if file_class else "low",
214
+ "relevance": round(max(0.0, min(1.0, combined / 2.0)), 3),
215
+ "reason": file_class.reason if file_class else (fs.reasons[0] if fs.reasons else "source file"),
216
+ "evidence": file_class.evidence if file_class else [],
217
+ }
218
+
219
+ ranking_reasons = [r for r in fs.reasons if r != "source file"]
220
+ if ranking_reasons:
221
+ item["ranking_reasons"] = ranking_reasons
222
+
223
+ scored.append((combined, item))
224
+
225
+ # Deterministic sort: score desc, then path asc
226
+ scored.sort(key=lambda x: (-x[0], x[1]["path"]))
227
+ return [item for _, item in scored[:limit]]
178
228
 
179
229
 
180
230
  def _architecture_context(sm: SourceMap) -> dict[str, Any]:
181
231
  arch = sm.architecture
182
232
  if arch is not None and arch.requested:
183
- pattern = arch.pattern if arch.pattern not in (None, "unknown", "flat") else "no confirmed architecture pattern; inferred partial layering"
184
- return {
233
+ pattern = arch.pattern if arch.pattern not in (None, "unknown", "flat") else None
234
+ ctx: dict[str, Any] = {
185
235
  "summary": sm.architecture_summary,
186
- "pattern": pattern,
236
+ "pattern": pattern or "insufficient_evidence",
187
237
  "confidence": arch.confidence,
188
238
  "method": arch.method,
189
- "layers": [
239
+ }
240
+ if arch.layers:
241
+ ctx["layers"] = [
190
242
  {
191
243
  "name": layer.name,
192
244
  "confidence": layer.confidence,
193
245
  "file_count": len(layer.files),
194
246
  }
195
247
  for layer in arch.layers
196
- ],
197
- "limitations": arch.limitations,
198
- }
248
+ ]
249
+ else:
250
+ ctx["no_layers_detected"] = True
251
+ if arch.confidence == "low" and not pattern:
252
+ ctx["note"] = "directory structure insufficient for reliable architectural inference; use --semantics for higher accuracy"
253
+ if arch.limitations:
254
+ ctx["limitations"] = arch.limitations
255
+ return ctx
199
256
  return {
200
257
  "summary": sm.architecture_summary,
201
- "pattern": "no confirmed architecture pattern; inferred partial layering",
258
+ "pattern": "insufficient_evidence",
202
259
  "confidence": "low",
203
260
  "method": "not_requested",
204
261
  "limitations": [
@@ -207,6 +264,18 @@ def _architecture_context(sm: SourceMap) -> dict[str, Any]:
207
264
  }
208
265
 
209
266
 
267
+ def _serialize_file_metric(m: Any) -> dict[str, Any]:
268
+ """Serialize FileMetrics, omitting null cyclomatic_complexity when availability is unavailable.
269
+
270
+ Prevents 100% of JS/TS/Go/Rust files from appearing as errors due to null complexity.
271
+ The complexity_availability field already communicates the reason — the null value adds noise.
272
+ """
273
+ d = asdict(m)
274
+ if d.get("complexity_availability") == "unavailable":
275
+ d.pop("cyclomatic_complexity", None)
276
+ return d
277
+
278
+
210
279
  def _section_confidence(sm: SourceMap) -> dict[str, str]:
211
280
  cs = sm.confidence_summary
212
281
  dep_conf = "low"
@@ -254,12 +323,33 @@ def compact_view(sm: SourceMap, *, no_tree: bool = False) -> dict[str, Any]:
254
323
  dep_summary_dict = None # "not analyzed" — agent should add --dependencies
255
324
 
256
325
  env_summary_dict: Any = None
326
+ env_map_items: Any = None
257
327
  if sm.env_summary is not None and sm.env_summary.requested:
258
328
  env_summary_dict = asdict(sm.env_summary)
329
+ if sm.env_map:
330
+ _sorted_env = sorted(
331
+ sm.env_map,
332
+ key=lambda e: (not getattr(e, "required", False), getattr(e, "key", "")),
333
+ )
334
+ env_map_items = [
335
+ {k: v for k, v in asdict(e).items() if v is not None and v != "" and v != []}
336
+ for e in _sorted_env[:15]
337
+ ]
259
338
 
260
339
  code_notes_summary_dict: Any = None
340
+ code_notes_items: Any = None
261
341
  if sm.code_notes_summary is not None and sm.code_notes_summary.requested:
262
342
  code_notes_summary_dict = asdict(sm.code_notes_summary)
343
+ if sm.code_notes:
344
+ _SEVERITY_ORDER = {"BUG": 0, "FIXME": 1, "DEPRECATED": 2, "TODO": 3, "HACK": 4, "WARNING": 5}
345
+ _sorted_notes = sorted(
346
+ sm.code_notes,
347
+ key=lambda n: (_SEVERITY_ORDER.get(getattr(n, "kind", "").upper(), 9), getattr(n, "path", "")),
348
+ )
349
+ code_notes_items = [
350
+ {k: v for k, v in asdict(n).items() if v is not None}
351
+ for n in _sorted_notes[:20]
352
+ ]
263
353
 
264
354
  # Entry points: production runtime only. Auxiliary and development entries
265
355
  # are exposed separately so agents do not mix tooling with execution paths.
@@ -298,7 +388,9 @@ def compact_view(sm: SourceMap, *, no_tree: bool = False) -> dict[str, Any]:
298
388
  "dependency_summary": dep_summary_dict,
299
389
  "key_dependencies": key_deps,
300
390
  "env_summary": env_summary_dict,
391
+ "env_map": env_map_items,
301
392
  "code_notes_summary": code_notes_summary_dict,
393
+ "code_notes": code_notes_items,
302
394
  "confidence_summary": conf_dict,
303
395
  "anomalies": anomalies,
304
396
  "analysis_gaps": gaps_list,
@@ -682,11 +774,33 @@ def agent_view(sm: SourceMap) -> dict[str, Any]:
682
774
  }
683
775
  if sm.env_summary.categories:
684
776
  signals["env_vars"]["categories"] = sm.env_summary.categories
777
+ if sm.env_map:
778
+ _sorted_env = sorted(
779
+ sm.env_map,
780
+ key=lambda e: (not getattr(e, "required", False), getattr(e, "key", "")),
781
+ )
782
+ signals["env_vars"]["keys"] = [
783
+ {k: v for k, v in asdict(e).items() if v is not None and v != "" and v != []}
784
+ for e in _sorted_env[:10]
785
+ ]
685
786
 
686
787
  if sm.code_notes_summary and sm.code_notes_summary.requested and sm.code_notes_summary.total > 0:
687
788
  by_kind = {k: v for k, v in sm.code_notes_summary.by_kind.items() if v > 0}
789
+ _code_notes_signal: dict[str, Any] = {}
688
790
  if by_kind:
689
- signals["code_notes"] = {"total": sm.code_notes_summary.total, "by_kind": by_kind}
791
+ _code_notes_signal = {"total": sm.code_notes_summary.total, "by_kind": by_kind}
792
+ if sm.code_notes:
793
+ _SEVERITY_ORDER = {"BUG": 0, "FIXME": 1, "DEPRECATED": 2, "TODO": 3, "HACK": 4, "WARNING": 5}
794
+ _sorted_notes = sorted(
795
+ sm.code_notes,
796
+ key=lambda n: (_SEVERITY_ORDER.get(getattr(n, "kind", "").upper(), 9), getattr(n, "path", "")),
797
+ )
798
+ _code_notes_signal["top"] = [
799
+ {k: v for k, v in asdict(n).items() if v is not None}
800
+ for n in _sorted_notes[:10]
801
+ ]
802
+ if _code_notes_signal:
803
+ signals["code_notes"] = _code_notes_signal
690
804
  if sm.code_notes_summary.adr_count > 0:
691
805
  signals["adrs"] = sm.code_notes_summary.adr_count
692
806
 
@@ -849,7 +963,7 @@ def standard_view(sm: SourceMap, *, include_tree: bool = False) -> dict[str, Any
849
963
 
850
964
  if sm.metrics_summary is not None and sm.metrics_summary.requested:
851
965
  result["metrics_summary"] = asdict(sm.metrics_summary)
852
- result["file_metrics"] = [asdict(m) for m in sm.file_metrics]
966
+ result["file_metrics"] = [_serialize_file_metric(m) for m in sm.file_metrics]
853
967
 
854
968
  if sm.architecture is not None and sm.architecture.requested:
855
969
  result["architecture"] = asdict(sm.architecture)
@@ -1131,6 +1245,12 @@ def _serialize_contract_minimal(c: Any) -> dict[str, Any]:
1131
1245
  if c.hooks_used:
1132
1246
  item["hooks"] = c.hooks_used
1133
1247
 
1248
+ # Ranking signals: why this file was ranked here
1249
+ if getattr(c, "ranking_reasons", None):
1250
+ non_trivial = [r for r in c.ranking_reasons if r not in ("source file", "noise")]
1251
+ if non_trivial:
1252
+ item["why"] = non_trivial
1253
+
1134
1254
  return item
1135
1255
 
1136
1256
 
@@ -1231,6 +1351,10 @@ def _contract_view_standard(
1231
1351
  item["dependencies"] = c.dependencies
1232
1352
  if c.limitations:
1233
1353
  item["limitations"] = c.limitations
1354
+ if getattr(c, "ranking_reasons", None):
1355
+ non_trivial = [r for r in c.ranking_reasons if r not in ("source file", "noise")]
1356
+ if non_trivial:
1357
+ item["ranking_reasons"] = non_trivial
1234
1358
  item["method"] = c.extraction_method
1235
1359
  serialized.append(item)
1236
1360
  result["file_contracts"] = serialized
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 0.38.0
3
+ Version: 0.41.0
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -1,33 +1,34 @@
1
- sourcecode/__init__.py,sha256=RjrfBH06OIJiq-xk4Hadj8Zl3Soer5r1Ct1ogF0xqaU,103
1
+ sourcecode/__init__.py,sha256=Z0LOxVp01ZH1jSUmGwFp1S832KRn_Hq6x3bcAaQ-10c,103
2
2
  sourcecode/adaptive_scanner.py,sha256=6dh34C2qZXyRbw-8xBhbEwDdXanM6CRFRWayVoYITnA,10190
3
3
  sourcecode/architecture_analyzer.py,sha256=H6noGgVArUJ25z1qC0fFA0KvJJeHZYyhKvKSkOyWHUk,23096
4
4
  sourcecode/architecture_summary.py,sha256=rSY5MRiaz4N1YdG0pqDTDuFjSN7PO_Zplx-dtNzv2Yo,19985
5
5
  sourcecode/ast_extractor.py,sha256=0OHQwTUBBc9lmqPLryVeB1z8dGIC6NhLlar800CD9oI,41129
6
6
  sourcecode/classifier.py,sha256=GKTMN8qKZX7ponSwDJfN08RrasI4CVpq1_gFBgEopps,7093
7
- sourcecode/cli.py,sha256=dJ0kkwC0pQ4LJyhjlbtHKSpD-TvRQQyhdhvjRCHPA8o,65280
7
+ sourcecode/cli.py,sha256=BBAS66tCeNt48iZrykJZ-H0TpI3zmgrAs6P3H3NSIws,67589
8
8
  sourcecode/code_notes_analyzer.py,sha256=rRd8bFYV0krjlxxQV0wenwE9K7pVpUQSR7KvSvUQKw4,9226
9
9
  sourcecode/confidence_analyzer.py,sha256=HxJMPLI5ulqtkncnv98W4iVO6yMbpQo87VuxiuNbDmY,12167
10
10
  sourcecode/context_summarizer.py,sha256=CiQrfBEzun949bWvmLabWoj2HhPn6Lw62ofqnsy0FlQ,6503
11
- sourcecode/contract_model.py,sha256=vr-9WHf0EBlbnuZGtTpUvSnbbjCBsm0q0tpLyxBJ-xI,3287
12
- sourcecode/contract_pipeline.py,sha256=Pu9SjgkngLgWoFaNj2ftKsk4lPngophW4840h0FvuEw,23187
11
+ sourcecode/contract_model.py,sha256=wpYNWGzHAVnyGxniGqNMk96TCmWbVVOqNSc3Kauajrg,3348
12
+ sourcecode/contract_pipeline.py,sha256=m2xPFLYWkTRvEv9L7iV9gqE0JRDxYhnx_IcQNo5P9es,22793
13
13
  sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
14
14
  sourcecode/dependency_analyzer.py,sha256=Exq0BfInvfS5iAg9xAr6WI2uPNuotkIudTKcYJcRhB8,52757
15
- sourcecode/doc_analyzer.py,sha256=Ec3orx6vBKsh5cNM3-F4y2Got2KuKx8w3dErwtdtM-A,19891
15
+ sourcecode/doc_analyzer.py,sha256=KLQ8g5cFTLEnZfH2xh7Z1t936oS6N6fP5L6YplhbtzM,20182
16
16
  sourcecode/entrypoint_classifier.py,sha256=a69dMGyxCTd_LOm3oqj-EXWpRmbmeujN7T1mr2eJ1as,3877
17
17
  sourcecode/env_analyzer.py,sha256=slvq-eT24RVMNczLNDlZbe0hU8JXIIPxybqubvrrnSQ,14409
18
18
  sourcecode/file_classifier.py,sha256=_KfFIIolharaIxbSTrCkaWauQIqNHCyor_n47RGyDh8,8577
19
- sourcecode/git_analyzer.py,sha256=s7tJTd_GAczhrH7j9JhBNp7ozhkW3lzBN0TMNwFqJwE,9977
19
+ sourcecode/git_analyzer.py,sha256=khF1AOT8dL5RP9d_tDqDpE8FXEvCa6Ns14L4BXjFcs0,11179
20
20
  sourcecode/graph_analyzer.py,sha256=hMOsLLz9B0UnQ4xwbHdgr3bFvqpw0bQ8kN-xmEn3Krk,64156
21
21
  sourcecode/metrics_analyzer.py,sha256=e2cFwB9XubFq_dIVsP2PLjpr4wX0N6ulb3ol3sGDUeo,20777
22
- sourcecode/prepare_context.py,sha256=vxEzr8czS3MFbdTx4hBJQlJLrl9cuvbHdL3ZokxFkvo,31384
22
+ sourcecode/prepare_context.py,sha256=zYRcRFc9OXN_V-3eKcVmA6wwO9A8uhUjM2cqkkp1dV0,30892
23
+ sourcecode/ranking_engine.py,sha256=XdhzahKGleYNW3N0GqGW9salPOXx2BNp8KqXpaeHHmw,8247
23
24
  sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
24
- sourcecode/relevance_scorer.py,sha256=ea7_7AHVgahVEWK3ebKOpG67agzG_pGICu5f2KgzrIA,8133
25
+ sourcecode/relevance_scorer.py,sha256=E74w7nlsNVobO3LqKHiMtBd84ONwGp8uDpwXJEjRtLA,8330
25
26
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
26
27
  sourcecode/runtime_classifier.py,sha256=zWX3r3HCKHc-qtIobErOa8aKMmaoPYREtJKvPcBGPjQ,14792
27
28
  sourcecode/scanner.py,sha256=aM3h9-DCQ3xKpeHpHYdo2vX6T5P95HA_YwZbkAVNwmo,8288
28
29
  sourcecode/schema.py,sha256=dVA-3EbHBakHLkgeZF-LfjKClEFRgPZkzblXpDTshFA,20796
29
- sourcecode/semantic_analyzer.py,sha256=asQfJf-EhzYaOTA-iMuZsrVXtbW7SV2WEKCxgsxa88Y,79413
30
- sourcecode/serializer.py,sha256=qJRJV_z-T_wU615KMA1ez5IIeV3wcexh29lY4-fcgjs,51329
30
+ sourcecode/semantic_analyzer.py,sha256=CBRRt92AFucf8vhKbly24132sM3EEIaZZpzFsUDpsUI,79617
31
+ sourcecode/serializer.py,sha256=1wWmBUTY1SoRBedVnE4_mPEzEL8xYsoZ8hamvpQiTvc,56477
31
32
  sourcecode/summarizer.py,sha256=ZuzIdm3t8A-d5MuQL0TSNLrd-L0IQIuguIxeNXMNJf8,16070
32
33
  sourcecode/tree_utils.py,sha256=Fj9OIuUksBvgibNd3feog0sMDjVypJzPexp5lvMoYWI,1424
33
34
  sourcecode/workspace.py,sha256=fQlVoNx8S-fSHpKoJ0JBvEHCFkxszH0KZVJed1i3TRk,6845
@@ -58,8 +59,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
58
59
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
59
60
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
60
61
  sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
61
- sourcecode-0.38.0.dist-info/METADATA,sha256=-RJ8bdDTHeuGmWN-iNo4eYkjPTuSnfriYYD1O59Gmwc,25209
62
- sourcecode-0.38.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
63
- sourcecode-0.38.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
64
- sourcecode-0.38.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
65
- sourcecode-0.38.0.dist-info/RECORD,,
62
+ sourcecode-0.41.0.dist-info/METADATA,sha256=NinjVy-jlbAy-be1L-ejAtO5j7HiAZwi5B3C4CbOCqk,25209
63
+ sourcecode-0.41.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
64
+ sourcecode-0.41.0.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
65
+ sourcecode-0.41.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
66
+ sourcecode-0.41.0.dist-info/RECORD,,