sourcecode 0.34.0__tar.gz → 0.35.0__tar.gz

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 (142) hide show
  1. {sourcecode-0.34.0 → sourcecode-0.35.0}/PKG-INFO +1 -1
  2. {sourcecode-0.34.0 → sourcecode-0.35.0}/pyproject.toml +1 -1
  3. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/__init__.py +1 -1
  4. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/ast_extractor.py +23 -3
  5. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/contract_pipeline.py +67 -19
  6. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/relevance_scorer.py +18 -5
  7. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/serializer.py +3 -1
  8. {sourcecode-0.34.0 → sourcecode-0.35.0}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
  9. {sourcecode-0.34.0 → sourcecode-0.35.0}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
  10. {sourcecode-0.34.0 → sourcecode-0.35.0}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
  11. {sourcecode-0.34.0 → sourcecode-0.35.0}/.gitignore +0 -0
  12. {sourcecode-0.34.0 → sourcecode-0.35.0}/.ruff.toml +0 -0
  13. {sourcecode-0.34.0 → sourcecode-0.35.0}/CONTRIBUTING.md +0 -0
  14. {sourcecode-0.34.0 → sourcecode-0.35.0}/LICENSE +0 -0
  15. {sourcecode-0.34.0 → sourcecode-0.35.0}/README.md +0 -0
  16. {sourcecode-0.34.0 → sourcecode-0.35.0}/SECURITY.md +0 -0
  17. {sourcecode-0.34.0 → sourcecode-0.35.0}/docs/privacy.md +0 -0
  18. {sourcecode-0.34.0 → sourcecode-0.35.0}/docs/schema.md +0 -0
  19. {sourcecode-0.34.0 → sourcecode-0.35.0}/raw +0 -0
  20. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/architecture_analyzer.py +0 -0
  21. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/architecture_summary.py +0 -0
  22. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/classifier.py +0 -0
  23. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/cli.py +0 -0
  24. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  25. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/confidence_analyzer.py +0 -0
  26. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/context_summarizer.py +0 -0
  27. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/contract_model.py +0 -0
  28. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/coverage_parser.py +0 -0
  29. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/dependency_analyzer.py +0 -0
  30. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/__init__.py +0 -0
  31. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/base.py +0 -0
  32. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  33. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/dart.py +0 -0
  34. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/dotnet.py +0 -0
  35. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/elixir.py +0 -0
  36. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/go.py +0 -0
  37. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/heuristic.py +0 -0
  38. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/hybrid.py +0 -0
  39. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/java.py +0 -0
  40. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  41. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/nodejs.py +0 -0
  42. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/parsers.py +0 -0
  43. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/php.py +0 -0
  44. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/project.py +0 -0
  45. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/python.py +0 -0
  46. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/ruby.py +0 -0
  47. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/rust.py +0 -0
  48. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/systems.py +0 -0
  49. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/terraform.py +0 -0
  50. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/detectors/tooling.py +0 -0
  51. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/doc_analyzer.py +0 -0
  52. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  53. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/env_analyzer.py +0 -0
  54. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/file_classifier.py +0 -0
  55. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/git_analyzer.py +0 -0
  56. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/graph_analyzer.py +0 -0
  57. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/metrics_analyzer.py +0 -0
  58. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/prepare_context.py +0 -0
  59. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/redactor.py +0 -0
  60. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/runtime_classifier.py +0 -0
  61. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/scanner.py +0 -0
  62. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/schema.py +0 -0
  63. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/semantic_analyzer.py +0 -0
  64. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/summarizer.py +0 -0
  65. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/telemetry/__init__.py +0 -0
  66. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/telemetry/config.py +0 -0
  67. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/telemetry/consent.py +0 -0
  68. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/telemetry/events.py +0 -0
  69. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/telemetry/filters.py +0 -0
  70. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/telemetry/transport.py +0 -0
  71. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/tree_utils.py +0 -0
  72. {sourcecode-0.34.0 → sourcecode-0.35.0}/src/sourcecode/workspace.py +0 -0
  73. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/__init__.py +0 -0
  74. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/conftest.py +0 -0
  75. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/fixtures/coverage.xml +0 -0
  76. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  77. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  78. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  79. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/fixtures/go_service/go.mod +0 -0
  80. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/fixtures/jacoco.xml +0 -0
  81. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/fixtures/lcov.info +0 -0
  82. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  83. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/fixtures/nextjs_app/package.json +0 -0
  84. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  85. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  86. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  87. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  88. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  89. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  90. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_architecture_analyzer.py +0 -0
  91. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_architecture_summary.py +0 -0
  92. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_ast_extractor.py +0 -0
  93. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_classifier.py +0 -0
  94. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_cli.py +0 -0
  95. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_code_notes_analyzer.py +0 -0
  96. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_contract_pipeline.py +0 -0
  97. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_coverage_parser.py +0 -0
  98. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_cross_consistency.py +0 -0
  99. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_dependency_analyzer_node_python.py +0 -0
  100. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  101. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_dependency_schema.py +0 -0
  102. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_detector_dotnet.py +0 -0
  103. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_detector_go_rust_java.py +0 -0
  104. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_detector_nodejs.py +0 -0
  105. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_detector_php_ruby_dart.py +0 -0
  106. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_detector_python.py +0 -0
  107. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_detector_universal_managed.py +0 -0
  108. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_detector_universal_systems.py +0 -0
  109. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_detectors_base.py +0 -0
  110. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  111. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_doc_analyzer_python.py +0 -0
  112. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  113. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_graph_analyzer_python_node.py +0 -0
  114. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_graph_schema.py +0 -0
  115. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_hybrid_inference.py +0 -0
  116. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_integration.py +0 -0
  117. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_integration_dependencies.py +0 -0
  118. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_integration_detection.py +0 -0
  119. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_integration_docs.py +0 -0
  120. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_integration_graph_modules.py +0 -0
  121. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_integration_lqn.py +0 -0
  122. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_integration_metrics.py +0 -0
  123. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_integration_multistack.py +0 -0
  124. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_integration_semantics.py +0 -0
  125. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_integration_universal.py +0 -0
  126. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_metrics_analyzer.py +0 -0
  127. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_packaging.py +0 -0
  128. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_phase1_improvements.py +0 -0
  129. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_pipeline_integrity.py +0 -0
  130. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_real_projects.py +0 -0
  131. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_redactor.py +0 -0
  132. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_scanner.py +0 -0
  133. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_schema.py +0 -0
  134. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_schema_normalization.py +0 -0
  135. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_semantic_analyzer_node.py +0 -0
  136. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_semantic_analyzer_python.py +0 -0
  137. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_semantic_import_resolution.py +0 -0
  138. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_semantic_schema.py +0 -0
  139. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_signal_hierarchy.py +0 -0
  140. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_summarizer.py +0 -0
  141. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_telemetry.py +0 -0
  142. {sourcecode-0.34.0 → sourcecode-0.35.0}/tests/test_workspace_analyzer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 0.34.0
3
+ Version: 0.35.0
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "0.34.0"
7
+ version = "0.35.0"
8
8
  description = "Deterministic codebase context for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "0.34.0"
3
+ __version__ = "0.35.0"
@@ -373,7 +373,8 @@ def _ts_types(root: Any, src: bytes) -> list[TypeDefinition]:
373
373
  continue
374
374
  name = _text(name_n, src)
375
375
  fields: list[TypeField] = []
376
- body_n = _find_child(node, "object_type")
376
+ # "interface_body" in tree-sitter-typescript >= 0.21; "object_type" in older builds
377
+ body_n = _find_child(node, "interface_body", "object_type")
377
378
  if body_n:
378
379
  for prop in _walk(body_n):
379
380
  if prop.type in ("property_signature", "method_signature"):
@@ -385,7 +386,7 @@ def _ts_types(root: Any, src: bytes) -> list[TypeDefinition]:
385
386
  required = not any(c.type == "?" for c in prop.children)
386
387
  fields.append(TypeField(name=prop_name, type=type_text, required=required))
387
388
  extends: list[str] = []
388
- heritage_n = _find_child(node, "extends_type_clause", "class_heritage")
389
+ heritage_n = _find_child(node, "extends_type_clause", "extends_clause", "class_heritage")
389
390
  if heritage_n:
390
391
  for ext_n in _walk(heritage_n):
391
392
  if ext_n.type == "type_identifier":
@@ -429,6 +430,25 @@ def _ts_hooks(root: Any, src: bytes) -> list[str]:
429
430
  return sorted(used)
430
431
 
431
432
 
433
+ def _merge_imports(imports: list[ImportRecord]) -> list[ImportRecord]:
434
+ """Merge multiple ImportRecords with the same source into one.
435
+
436
+ Tree-sitter correctly captures `import { A }` and `import type { B }` from
437
+ the same module as two separate statements. Merging them produces a compact,
438
+ predictable contract where each source appears exactly once.
439
+ """
440
+ merged: dict[str, ImportRecord] = {}
441
+ for imp in imports:
442
+ if imp.source in merged:
443
+ existing = merged[imp.source]
444
+ combined_symbols = sorted(set(existing.symbols) | set(imp.symbols))
445
+ kind = existing.kind if existing.kind != "side_effect" else imp.kind
446
+ merged[imp.source] = ImportRecord(source=imp.source, symbols=combined_symbols, kind=kind)
447
+ else:
448
+ merged[imp.source] = imp
449
+ return list(merged.values())
450
+
451
+
432
452
  def _extract_ts_js_tree_sitter(path: str, source: str, lang_obj: Any, language: str) -> FileContract:
433
453
  try:
434
454
  parser = _get_parser(lang_obj)
@@ -436,7 +456,7 @@ def _extract_ts_js_tree_sitter(path: str, source: str, lang_obj: Any, language:
436
456
  tree = parser.parse(src_bytes)
437
457
  root = tree.root_node
438
458
 
439
- imports = _ts_imports(root, src_bytes)
459
+ imports = _merge_imports(_ts_imports(root, src_bytes))
440
460
  exports = _ts_exports(root, src_bytes)
441
461
  exported_names = {e.name for e in exports}
442
462
  functions = _ts_functions(root, src_bytes, exported_names)
@@ -25,6 +25,23 @@ from sourcecode.schema import EntryPoint, MonorepoPackageInfo
25
25
  _MAX_FILES = 500 # hard cap on files extracted per run
26
26
  _SRC_EXTENSIONS: frozenset[str] = frozenset(_LANGUAGE_MAP.keys())
27
27
 
28
+ # Role-based score adjustments applied after contract extraction.
29
+ # Runtime roles get a boost; config/util are neutral or penalized.
30
+ _ROLE_SCORE: dict[str, float] = {
31
+ "entrypoint": 0.15,
32
+ "service": 0.10,
33
+ "route": 0.10,
34
+ "api": 0.08,
35
+ "middleware": 0.06,
36
+ "store": 0.05,
37
+ "model": 0.05,
38
+ "hook": 0.05,
39
+ "component": 0.03,
40
+ "util": 0.00,
41
+ "config": -0.10,
42
+ "unknown": 0.00,
43
+ }
44
+
28
45
  RankStrategy = Literal["relevance", "centrality", "git-churn"]
29
46
 
30
47
 
@@ -206,9 +223,9 @@ class ContractPipeline:
206
223
  if changed_only:
207
224
  src_paths = [p for p in src_paths if p in changed_files]
208
225
 
209
- # Apply max_files cap
210
- if len(src_paths) > self.max_files:
211
- # Pre-rank by static relevance to pick best candidates
226
+ # Apply max_files cap — bypass when symbol search to ensure defining files are found.
227
+ # A symbol query over a large repo needs all files; result set is small after filtering.
228
+ if symbol is None and len(src_paths) > self.max_files:
212
229
  src_paths = sorted(
213
230
  src_paths,
214
231
  key=lambda p: (p in entry_paths, scorer.score(p)),
@@ -255,23 +272,9 @@ class ContractPipeline:
255
272
  # 7. Rank
256
273
  contracts = self._rank(contracts, rank_by)
257
274
 
258
- # 8. Symbol filter — keep files that export or use the symbol
275
+ # 8. Symbol filter — keep files that define or import the symbol
259
276
  if symbol:
260
- symbol_contracts = [
261
- c for c in contracts
262
- if any(e.name == symbol for e in c.exports)
263
- or any(f.name == symbol for f in c.functions)
264
- or symbol in {t.name for t in c.types}
265
- ]
266
- # Also pull in direct importers (fan_in sourcing)
267
- importer_paths = {
268
- c.path for c in contracts
269
- for imp in c.imports
270
- if symbol in imp.symbols
271
- }
272
- importer_contracts = [c for c in contracts if c.path in importer_paths]
273
- symbol_contracts = list({c.path: c for c in symbol_contracts + importer_contracts}.values())
274
- contracts = sorted(symbol_contracts, key=lambda c: -c.relevance_score)
277
+ contracts = _filter_by_symbol(contracts, symbol)
275
278
 
276
279
  # 9. Entrypoints-only filter
277
280
  if entrypoints_only and not symbol:
@@ -323,6 +326,9 @@ class ContractPipeline:
323
326
  churn_score = min(churn.get(c.path, 0) / 20.0, 0.1)
324
327
  base += churn_score
325
328
 
329
+ # Role-based boost: runtime roles score higher than auxiliary
330
+ base += _ROLE_SCORE.get(c.role, 0.0)
331
+
326
332
  return min(1.0, base)
327
333
 
328
334
  def _rank(self, contracts: list[FileContract], rank_by: RankStrategy) -> list[FileContract]:
@@ -385,6 +391,48 @@ def _limit_symbols(contracts: list[FileContract], max_symbols: int) -> list[File
385
391
  return result
386
392
 
387
393
 
394
+ # ---------------------------------------------------------------------------
395
+ # Symbol-aware filter
396
+ # ---------------------------------------------------------------------------
397
+
398
+ def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileContract]:
399
+ """Return contracts that define or import *symbol*.
400
+
401
+ Matching strategy:
402
+ 1. Exact match on export/function/type names.
403
+ 2. Case-insensitive fallback when exact match yields nothing.
404
+ 3. Importer contracts: files that name the symbol in their imports.
405
+
406
+ Defining contracts are ranked first; importers follow.
407
+ """
408
+ def _defines(c: FileContract, sym: str, case: bool) -> bool:
409
+ cmp = (lambda a, b: a.lower() == b.lower()) if case else (lambda a, b: a == b)
410
+ return (
411
+ any(cmp(e.name, sym) for e in c.exports)
412
+ or any(cmp(f.name, sym) for f in c.functions)
413
+ or any(cmp(t.name, sym) for t in c.types)
414
+ )
415
+
416
+ def _imports(c: FileContract, sym: str, case: bool) -> bool:
417
+ if case:
418
+ sym_l = sym.lower()
419
+ return any(sym_l == s.lower() for imp in c.imports for s in imp.symbols)
420
+ return any(sym in imp.symbols for imp in c.imports)
421
+
422
+ # Exact match first
423
+ defining = [c for c in contracts if _defines(c, symbol, case=False)]
424
+ if not defining:
425
+ defining = [c for c in contracts if _defines(c, symbol, case=True)]
426
+
427
+ importer_paths = {c.path for c in contracts if _imports(c, symbol, case=len(defining) == 0)}
428
+ # Exclude files already in defining set
429
+ defining_paths = {c.path for c in defining}
430
+ importers = [c for c in contracts if c.path in importer_paths and c.path not in defining_paths]
431
+
432
+ merged = list({c.path: c for c in defining + importers}.values())
433
+ return sorted(merged, key=lambda c: (c.path not in defining_paths, -c.relevance_score))
434
+
435
+
388
436
  # ---------------------------------------------------------------------------
389
437
  # Dependency graph emission
390
438
  # ---------------------------------------------------------------------------
@@ -104,6 +104,18 @@ _LOW_RUNTIME_STEMS: frozenset[str] = frozenset({
104
104
  "gruntfile", "gulpfile", "webpack.config", "vite.config",
105
105
  "rollup.config", "babel.config", "jest.config", "vitest.config",
106
106
  "tsconfig", "jsconfig", ".eslintrc", ".prettierrc", ".editorconfig",
107
+ # doc-site tooling
108
+ "rspress", "rspress.config", "docusaurus.config", "docusaurus",
109
+ "vuepress.config", "vuepress", "nextra.config",
110
+ "astro.config", "gatsby.config", "gatsby-config",
111
+ # build/workspace orchestration
112
+ "turbo", "turbo.config", "nx", "nx.config", "lerna",
113
+ "esbuild.config", "swc.config", "postcss.config",
114
+ "tailwind.config", "tailwind",
115
+ # storybook
116
+ "main.storybook", "preview.storybook",
117
+ # playwright / cypress / e2e
118
+ "playwright.config", "cypress.config",
107
119
  })
108
120
 
109
121
  _HIGH_VALUE_SUFFIXES: frozenset[str] = frozenset({
@@ -169,15 +181,16 @@ class RelevanceScorer:
169
181
  if (any(m in f"/{norm}/" for m in _TEST_DIR_MARKERS)
170
182
  or any(fname.startswith(p.strip(".")) or p in fname
171
183
  for p in _TEST_FILE_PATTERNS)):
172
- base -= 0.25
184
+ base -= 0.30
173
185
 
174
- # Config/tooling filename penalty
186
+ # Config/tooling filename penalty — stronger than before
175
187
  if stem.lower() in _LOW_RUNTIME_STEMS:
176
- base -= 0.2
188
+ base -= 0.30
177
189
 
178
- # Auxiliary dir penalty
190
+ # Auxiliary dir penalty (docs, examples, demos, fixtures, scripts…)
191
+ # Aggressive: these almost never belong in top-ranked agent context
179
192
  if self._is_auxiliary(norm):
180
- base -= 0.2
193
+ base -= 0.40
181
194
 
182
195
  return max(0.0, min(1.0, base))
183
196
 
@@ -964,7 +964,9 @@ def _contract_view_minimal(
964
964
  # Compact summary
965
965
  if sm.contract_summary is not None:
966
966
  cs = sm.contract_summary
967
- degraded = bool(cs.method_breakdown.get("heuristic", 0))
967
+ # degraded only when tree-sitter is actually unavailable — not when individual
968
+ # files fall back due to parse errors or size limits.
969
+ degraded = any("tree_sitter_unavailable" in lim for lim in cs.limitations)
968
970
  summary: dict[str, Any] = {
969
971
  "files": cs.extracted_files,
970
972
  "total": cs.total_files,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes