sourcecode 0.42.0__tar.gz → 0.43.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 (148) hide show
  1. {sourcecode-0.42.0 → sourcecode-0.43.0}/PKG-INFO +1 -1
  2. {sourcecode-0.42.0 → sourcecode-0.43.0}/pyproject.toml +1 -1
  3. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/__init__.py +1 -1
  4. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/cli.py +28 -0
  5. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/contract_model.py +1 -0
  6. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/contract_pipeline.py +40 -8
  7. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/serializer.py +29 -4
  8. sourcecode-0.43.0/tests/test_block5_quality.py +302 -0
  9. {sourcecode-0.42.0 → sourcecode-0.43.0}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
  10. {sourcecode-0.42.0 → sourcecode-0.43.0}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
  11. {sourcecode-0.42.0 → sourcecode-0.43.0}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
  12. {sourcecode-0.42.0 → sourcecode-0.43.0}/.gitignore +0 -0
  13. {sourcecode-0.42.0 → sourcecode-0.43.0}/.ruff.toml +0 -0
  14. {sourcecode-0.42.0 → sourcecode-0.43.0}/CONTRIBUTING.md +0 -0
  15. {sourcecode-0.42.0 → sourcecode-0.43.0}/LICENSE +0 -0
  16. {sourcecode-0.42.0 → sourcecode-0.43.0}/README.md +0 -0
  17. {sourcecode-0.42.0 → sourcecode-0.43.0}/SECURITY.md +0 -0
  18. {sourcecode-0.42.0 → sourcecode-0.43.0}/docs/privacy.md +0 -0
  19. {sourcecode-0.42.0 → sourcecode-0.43.0}/docs/schema.md +0 -0
  20. {sourcecode-0.42.0 → sourcecode-0.43.0}/raw +0 -0
  21. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/adaptive_scanner.py +0 -0
  22. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/architecture_analyzer.py +0 -0
  23. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/architecture_summary.py +0 -0
  24. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/ast_extractor.py +0 -0
  25. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/classifier.py +0 -0
  26. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  27. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/confidence_analyzer.py +0 -0
  28. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/context_summarizer.py +0 -0
  29. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/coverage_parser.py +0 -0
  30. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/dependency_analyzer.py +0 -0
  31. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/__init__.py +0 -0
  32. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/base.py +0 -0
  33. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  34. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/dart.py +0 -0
  35. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/dotnet.py +0 -0
  36. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/elixir.py +0 -0
  37. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/go.py +0 -0
  38. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/heuristic.py +0 -0
  39. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/hybrid.py +0 -0
  40. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/java.py +0 -0
  41. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  42. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/nodejs.py +0 -0
  43. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/parsers.py +0 -0
  44. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/php.py +0 -0
  45. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/project.py +0 -0
  46. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/python.py +0 -0
  47. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/ruby.py +0 -0
  48. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/rust.py +0 -0
  49. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/systems.py +0 -0
  50. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/terraform.py +0 -0
  51. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/detectors/tooling.py +0 -0
  52. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/doc_analyzer.py +0 -0
  53. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  54. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/env_analyzer.py +0 -0
  55. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/file_classifier.py +0 -0
  56. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/git_analyzer.py +0 -0
  57. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/graph_analyzer.py +0 -0
  58. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/metrics_analyzer.py +0 -0
  59. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/prepare_context.py +0 -0
  60. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/ranking_engine.py +0 -0
  61. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/redactor.py +0 -0
  62. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/relevance_scorer.py +0 -0
  63. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/repo_classifier.py +0 -0
  64. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/runtime_classifier.py +0 -0
  65. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/scanner.py +0 -0
  66. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/schema.py +0 -0
  67. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/semantic_analyzer.py +0 -0
  68. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/summarizer.py +0 -0
  69. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/__init__.py +0 -0
  70. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/config.py +0 -0
  71. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/consent.py +0 -0
  72. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/events.py +0 -0
  73. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/filters.py +0 -0
  74. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/transport.py +0 -0
  75. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/tree_utils.py +0 -0
  76. {sourcecode-0.42.0 → sourcecode-0.43.0}/src/sourcecode/workspace.py +0 -0
  77. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/__init__.py +0 -0
  78. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/conftest.py +0 -0
  79. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/fixtures/coverage.xml +0 -0
  80. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  81. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  82. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  83. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/fixtures/go_service/go.mod +0 -0
  84. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/fixtures/jacoco.xml +0 -0
  85. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/fixtures/lcov.info +0 -0
  86. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  87. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/fixtures/nextjs_app/package.json +0 -0
  88. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  89. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  90. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  91. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  92. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  93. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  94. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_architecture_analyzer.py +0 -0
  95. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_architecture_summary.py +0 -0
  96. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_ast_extractor.py +0 -0
  97. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_block1_reliability.py +0 -0
  98. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_block2_coverage.py +0 -0
  99. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_classifier.py +0 -0
  100. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_cli.py +0 -0
  101. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_code_notes_analyzer.py +0 -0
  102. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_contract_pipeline.py +0 -0
  103. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_coverage_parser.py +0 -0
  104. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_cross_consistency.py +0 -0
  105. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_dependency_analyzer_node_python.py +0 -0
  106. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  107. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_dependency_schema.py +0 -0
  108. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_detector_dotnet.py +0 -0
  109. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_detector_go_rust_java.py +0 -0
  110. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_detector_nodejs.py +0 -0
  111. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_detector_php_ruby_dart.py +0 -0
  112. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_detector_python.py +0 -0
  113. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_detector_universal_managed.py +0 -0
  114. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_detector_universal_systems.py +0 -0
  115. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_detectors_base.py +0 -0
  116. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  117. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_doc_analyzer_python.py +0 -0
  118. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  119. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_graph_analyzer_python_node.py +0 -0
  120. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_graph_schema.py +0 -0
  121. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_hybrid_inference.py +0 -0
  122. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_integration.py +0 -0
  123. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_integration_dependencies.py +0 -0
  124. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_integration_detection.py +0 -0
  125. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_integration_docs.py +0 -0
  126. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_integration_graph_modules.py +0 -0
  127. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_integration_lqn.py +0 -0
  128. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_integration_metrics.py +0 -0
  129. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_integration_multistack.py +0 -0
  130. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_integration_semantics.py +0 -0
  131. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_integration_universal.py +0 -0
  132. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_metrics_analyzer.py +0 -0
  133. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_packaging.py +0 -0
  134. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_phase1_improvements.py +0 -0
  135. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_pipeline_integrity.py +0 -0
  136. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_real_projects.py +0 -0
  137. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_redactor.py +0 -0
  138. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_scanner.py +0 -0
  139. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_schema.py +0 -0
  140. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_schema_normalization.py +0 -0
  141. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_semantic_analyzer_node.py +0 -0
  142. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_semantic_analyzer_python.py +0 -0
  143. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_semantic_import_resolution.py +0 -0
  144. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_semantic_schema.py +0 -0
  145. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_signal_hierarchy.py +0 -0
  146. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_summarizer.py +0 -0
  147. {sourcecode-0.42.0 → sourcecode-0.43.0}/tests/test_telemetry.py +0 -0
  148. {sourcecode-0.42.0 → sourcecode-0.43.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.42.0
3
+ Version: 0.43.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.42.0"
7
+ version = "0.43.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.42.0"
3
+ __version__ = "0.43.0"
@@ -181,6 +181,7 @@ _OPTIONS_WITH_VALUE: frozenset[str] = frozenset({
181
181
  "--dependency-depth",
182
182
  "--rank-by",
183
183
  "--symbol",
184
+ "--max-importers",
184
185
  })
185
186
 
186
187
 
@@ -594,6 +595,17 @@ def main(
594
595
  "--symbol",
595
596
  help="Contract mode: extract localized context for a specific symbol name. Returns defining file + all importers.",
596
597
  ),
598
+ max_importers: int = typer.Option(
599
+ 50,
600
+ "--max-importers",
601
+ help=(
602
+ "Maximum importer files returned by --symbol (default: 50). "
603
+ "Popular symbols can have hundreds of importers — this prevents output explosion. "
604
+ "Defining files are never truncated. Override: --symbol Foo --max-importers 200."
605
+ ),
606
+ min=1,
607
+ max=10000,
608
+ ),
597
609
  copy: bool = typer.Option(
598
610
  False,
599
611
  "--copy",
@@ -770,6 +782,21 @@ def main(
770
782
  code_notes = True
771
783
  no_tree = True # agents never need the raw file tree
772
784
  typer.echo("[agent] dependencies env-map code-notes (no-tree)", err=True)
785
+ # Warn about flags that are computed but excluded from agent_view output
786
+ _agent_suppressed: list[str] = []
787
+ if full_metrics:
788
+ _agent_suppressed.append("--full-metrics")
789
+ if graph_modules:
790
+ _agent_suppressed.append("--graph-modules")
791
+ if docs:
792
+ _agent_suppressed.append("--docs")
793
+ if _agent_suppressed:
794
+ typer.echo(
795
+ f"[agent] warning: {', '.join(_agent_suppressed)} computed but excluded "
796
+ "from --agent output — agent_view does not include these sections. "
797
+ "Remove these flags to skip unnecessary computation.",
798
+ err=True,
799
+ )
773
800
 
774
801
  scanner = AdaptiveScanner(target, topology=_topology, base_depth=effective_depth)
775
802
  raw_tree = scanner.scan_tree()
@@ -1343,6 +1370,7 @@ def main(
1343
1370
  changed_only=changed_only,
1344
1371
  symbol=symbol,
1345
1372
  compress_types=compress_types,
1373
+ max_importers=max_importers,
1346
1374
  )
1347
1375
  sm = _replace(sm, file_contracts=_contracts, contract_summary=_contract_summary)
1348
1376
  if symbol is not None and len(_contracts) == 0:
@@ -109,3 +109,4 @@ class ContractSummary:
109
109
  method_breakdown: dict[str, int] = field(default_factory=dict)
110
110
  ranked_by: str = "relevance"
111
111
  limitations: list[str] = field(default_factory=list)
112
+ symbol_truncation: Optional[dict] = None # set when --symbol truncates importers
@@ -175,6 +175,7 @@ class ContractPipeline:
175
175
  changed_only: bool = False,
176
176
  symbol: Optional[str] = None,
177
177
  compress_types: bool = False,
178
+ max_importers: int = 50,
178
179
  ) -> tuple[list[FileContract], ContractSummary]:
179
180
  """Run the full extraction pipeline.
180
181
 
@@ -279,17 +280,19 @@ class ContractPipeline:
279
280
  contracts = self._rank(contracts, rank_by)
280
281
 
281
282
  # 8. Symbol filter — keep files that define or import the symbol
283
+ _symbol_truncation: Optional[dict] = None
282
284
  if symbol:
283
- contracts = _filter_by_symbol(contracts, symbol)
285
+ contracts, _symbol_truncation = _filter_by_symbol(contracts, symbol, max_importers=max_importers)
284
286
  # When shallow scan missed the defining file (deep monorepo), fall back
285
287
  # to a grep-based filesystem search over the full directory tree.
286
288
  if not contracts:
287
- contracts = self._symbol_deep_scan(
289
+ contracts, _symbol_truncation = self._symbol_deep_scan(
288
290
  root, symbol,
289
291
  known_paths=set(src_paths),
290
292
  entry_paths=entry_paths,
291
293
  changed_files=changed_files,
292
294
  engine=engine,
295
+ max_importers=max_importers,
293
296
  )
294
297
 
295
298
  # 9. Entrypoints-only filter
@@ -313,6 +316,7 @@ class ContractPipeline:
313
316
  method_breakdown=dict(method_counts),
314
317
  ranked_by=rank_by,
315
318
  limitations=limitations,
319
+ symbol_truncation=_symbol_truncation,
316
320
  )
317
321
  return contracts, summary
318
322
 
@@ -332,7 +336,8 @@ class ContractPipeline:
332
336
  entry_paths: set[str],
333
337
  changed_files: set[str],
334
338
  engine: RankingEngine,
335
- ) -> list[FileContract]:
339
+ max_importers: int = 50,
340
+ ) -> tuple[list[FileContract], dict]:
336
341
  """Grep-based fallback when the shallow scan missed the defining files.
337
342
 
338
343
  Searches the full directory tree for source files containing *symbol*,
@@ -356,7 +361,7 @@ class ContractPipeline:
356
361
  contract.ranking_reasons = fs.reasons
357
362
  extra.append(contract)
358
363
 
359
- return _filter_by_symbol(extra, symbol)
364
+ return _filter_by_symbol(extra, symbol, max_importers=max_importers)
360
365
 
361
366
 
362
367
  # ---------------------------------------------------------------------------
@@ -412,7 +417,11 @@ def _limit_symbols(contracts: list[FileContract], max_symbols: int) -> list[File
412
417
  # Symbol-aware filter
413
418
  # ---------------------------------------------------------------------------
414
419
 
415
- def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileContract]:
420
+ def _filter_by_symbol(
421
+ contracts: list[FileContract],
422
+ symbol: str,
423
+ max_importers: int = 50,
424
+ ) -> tuple[list[FileContract], dict]:
416
425
  """Return contracts that define, import, or structurally reference *symbol*.
417
426
 
418
427
  Four tiers applied in order:
@@ -423,6 +432,8 @@ def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileCo
423
432
  function signatures (word-boundary). Only used when tiers 1-3 fail.
424
433
 
425
434
  Defining contracts are ranked first; importers and references follow.
435
+ max_importers caps tier 3 results to prevent output explosion on popular symbols.
436
+ Returns (contracts, truncation_metadata).
426
437
  """
427
438
  sym_l = symbol.lower()
428
439
  word_re = re.compile(
@@ -466,8 +477,14 @@ def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileCo
466
477
 
467
478
  # Tier 3: import matching (case-insensitive when no definers found)
468
479
  ci_imports = len(defining) == 0
469
- importer_paths = {c.path for c in contracts if _imports_sym(c, case=ci_imports)}
470
- importers = [c for c in contracts if c.path in importer_paths and c.path not in defining_paths]
480
+ all_importer_paths = {c.path for c in contracts if _imports_sym(c, case=ci_imports)}
481
+ all_importers = [c for c in contracts if c.path in all_importer_paths and c.path not in defining_paths]
482
+
483
+ # Apply importer cap — definers are never truncated
484
+ total_importers = len(all_importers)
485
+ truncated = total_importers > max_importers
486
+ importers = all_importers[:max_importers] if truncated else all_importers
487
+ importer_paths = {c.path for c in importers}
471
488
 
472
489
  # Tier 4: type-reference matching (only when tiers 1-3 yield nothing)
473
490
  references: list[FileContract] = []
@@ -483,12 +500,27 @@ def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileCo
483
500
  seen.add(c.path)
484
501
  merged.append(c)
485
502
 
486
- return sorted(merged, key=lambda c: (
503
+ result = sorted(merged, key=lambda c: (
487
504
  c.path not in defining_paths,
488
505
  c.path not in importer_paths,
489
506
  -c.relevance_score,
490
507
  ))
491
508
 
509
+ truncation: dict = {
510
+ "symbol": symbol,
511
+ "definers_found": len(defining),
512
+ "importers_found": total_importers,
513
+ "importers_returned": len(importers),
514
+ "references_found": len(references),
515
+ "total_returned": len(result),
516
+ "truncated": truncated,
517
+ }
518
+ if truncated:
519
+ truncation["truncation_reason"] = "max_importers_limit"
520
+ truncation["override_hint"] = f"--symbol {symbol} --max-importers {total_importers}"
521
+
522
+ return result, truncation
523
+
492
524
 
493
525
  # ---------------------------------------------------------------------------
494
526
  # Deep symbol scan — grep-based fallback for shallow-scanned repos
@@ -722,8 +722,10 @@ def agent_view(sm: SourceMap) -> dict[str, Any]:
722
722
  # production runtime is represented as entry_points=[], never by fallback.
723
723
  ep_groups = _entry_point_groups(sm.entry_points)
724
724
  result["entry_points"] = ep_groups["production"]
725
- result["development_entry_points"] = ep_groups["development"]
726
- result["auxiliary_entry_points"] = ep_groups["auxiliary"]
725
+ if ep_groups["development"]:
726
+ result["development_entry_points"] = ep_groups["development"]
727
+ if ep_groups["auxiliary"]:
728
+ result["auxiliary_entry_points"] = ep_groups["auxiliary"]
727
729
 
728
730
  # ── 3. Architecture ───────────────────────────────────────────────────────
729
731
  result["architecture"] = _architecture_context(sm)
@@ -888,6 +890,23 @@ def agent_view(sm: SourceMap) -> dict[str, Any]:
888
890
  if analysis_gaps:
889
891
  result["analysis_gaps"] = analysis_gaps
890
892
 
893
+ # ── 8. Agent mode metadata — explicit transparency about auto-enabled/suppressed flags ──
894
+ _auto_enabled: list[str] = ["--dependencies", "--env-map", "--code-notes"]
895
+ _suppressed: list[str] = []
896
+ if sm.metrics_summary is not None and sm.metrics_summary.requested:
897
+ _suppressed.append("--full-metrics")
898
+ if sm.module_graph is not None and sm.module_graph.summary.requested:
899
+ _suppressed.append("--graph-modules")
900
+ if sm.doc_summary is not None and sm.doc_summary.requested:
901
+ _suppressed.append("--docs")
902
+ agent_mode_meta: dict[str, Any] = {
903
+ "auto_enabled": _auto_enabled,
904
+ }
905
+ if _suppressed:
906
+ agent_mode_meta["suppressed_flags"] = _suppressed
907
+ agent_mode_meta["suppressed_note"] = "computed but excluded from agent_view"
908
+ result["agent_mode"] = agent_mode_meta
909
+
891
910
  return result
892
911
 
893
912
 
@@ -918,9 +937,11 @@ def standard_view(sm: SourceMap, *, include_tree: bool = False) -> dict[str, Any
918
937
  "architecture_summary": sm.architecture_summary,
919
938
  "stacks": [asdict(s) for s in sm.stacks],
920
939
  "entry_points": ep_groups["production"],
921
- "development_entry_points": ep_groups["development"],
922
- "auxiliary_entry_points": ep_groups["auxiliary"],
923
940
  }
941
+ if ep_groups["development"]:
942
+ result["development_entry_points"] = ep_groups["development"]
943
+ if ep_groups["auxiliary"]:
944
+ result["auxiliary_entry_points"] = ep_groups["auxiliary"]
924
945
 
925
946
  # Layer B — signals (only when the corresponding analyzer ran)
926
947
  if sm.dependency_summary is not None and sm.dependency_summary.requested:
@@ -1125,6 +1146,8 @@ def _contract_view_minimal(
1125
1146
  summary["degraded"] = True
1126
1147
  summary["degraded_hint"] = "install sourcecode[ast] for full TS/JS extraction"
1127
1148
  result["summary"] = summary
1149
+ if cs.symbol_truncation:
1150
+ result["symbol_query"] = cs.symbol_truncation
1128
1151
 
1129
1152
  return result
1130
1153
 
@@ -1404,6 +1427,8 @@ def _contract_view_standard(
1404
1427
  }
1405
1428
  if cs.limitations:
1406
1429
  result["contract_summary"]["limitations"] = cs.limitations
1430
+ if cs.symbol_truncation:
1431
+ result["symbol_query"] = cs.symbol_truncation
1407
1432
 
1408
1433
  return result
1409
1434
 
@@ -0,0 +1,302 @@
1
+ from __future__ import annotations
2
+
3
+ """Tests for block 5 quality improvements:
4
+ 1. --symbol truncation via max_importers
5
+ 2. agent_view suppressed flags metadata
6
+ 3. Empty entry point lists omitted from output
7
+ 4. symbol_query in contract views
8
+ """
9
+
10
+ from dataclasses import replace
11
+ from pathlib import Path
12
+ from typing import Any, Optional
13
+
14
+ import pytest
15
+
16
+ from sourcecode.contract_model import (
17
+ ContractSummary,
18
+ ExportRecord,
19
+ FileContract,
20
+ ImportRecord,
21
+ )
22
+ from sourcecode.contract_pipeline import _filter_by_symbol
23
+ from sourcecode.schema import (
24
+ AnalysisMetadata,
25
+ DocSummary,
26
+ EntryPoint,
27
+ MetricsSummary,
28
+ ModuleGraph,
29
+ ModuleGraphSummary,
30
+ SourceMap,
31
+ StackDetection,
32
+ )
33
+ from sourcecode.serializer import agent_view, standard_view, contract_view
34
+
35
+
36
+ # ---------------------------------------------------------------------------
37
+ # Helpers
38
+ # ---------------------------------------------------------------------------
39
+
40
+ def _make_definer(path: str, symbol: str) -> FileContract:
41
+ c = FileContract(path=path, language="python")
42
+ c.exports = [ExportRecord(name=symbol, kind="class")]
43
+ return c
44
+
45
+
46
+ def _make_importer(path: str, symbol: str) -> FileContract:
47
+ c = FileContract(path=path, language="python")
48
+ c.imports = [ImportRecord(source="mymodule", symbols=[symbol])]
49
+ return c
50
+
51
+
52
+ def _make_sm(**kwargs: Any) -> SourceMap:
53
+ """Minimal SourceMap for serializer tests."""
54
+ defaults: dict[str, Any] = {
55
+ "metadata": AnalysisMetadata(analyzed_path="/tmp/test"),
56
+ "stacks": [StackDetection(stack="python", primary=True)],
57
+ "entry_points": [],
58
+ "file_paths": [],
59
+ "project_type": "python",
60
+ "project_summary": "test project",
61
+ "architecture_summary": "simple",
62
+ }
63
+ defaults.update(kwargs)
64
+ return SourceMap(**defaults)
65
+
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # 1. --symbol truncation: max_importers caps output
69
+ # ---------------------------------------------------------------------------
70
+
71
+ def test_symbol_truncation_limits_importers() -> None:
72
+ """max_importers=2 with 5 importers → truncated=True, only 2 importers returned."""
73
+ definer = _make_definer("mymodule.py", "MyClass")
74
+ importers = [_make_importer(f"user{i}.py", "MyClass") for i in range(5)]
75
+
76
+ result, meta = _filter_by_symbol([definer] + importers, "MyClass", max_importers=2)
77
+
78
+ assert meta["truncated"] is True
79
+ assert meta["importers_found"] == 5
80
+ assert meta["importers_returned"] == 2
81
+ assert meta["definers_found"] == 1
82
+ # Definer + 2 importers = 3 total
83
+ assert meta["total_returned"] == 3
84
+ assert meta["truncation_reason"] == "max_importers_limit"
85
+ assert "override_hint" in meta
86
+ # Result list matches total_returned
87
+ assert len(result) == 3
88
+
89
+
90
+ def test_symbol_no_truncation_small() -> None:
91
+ """When importers are under the cap, truncated=False."""
92
+ definer = _make_definer("mymodule.py", "SmallClass")
93
+ importers = [_make_importer(f"user{i}.py", "SmallClass") for i in range(3)]
94
+
95
+ result, meta = _filter_by_symbol([definer] + importers, "SmallClass", max_importers=50)
96
+
97
+ assert meta["truncated"] is False
98
+ assert meta["importers_found"] == 3
99
+ assert meta["importers_returned"] == 3
100
+ assert "truncation_reason" not in meta
101
+ assert "override_hint" not in meta
102
+
103
+
104
+ def test_symbol_definers_not_truncated() -> None:
105
+ """Defining files are always included even when importers are capped to 0."""
106
+ definer = _make_definer("core.py", "CoreClass")
107
+ importers = [_make_importer(f"consumer{i}.py", "CoreClass") for i in range(10)]
108
+
109
+ result, meta = _filter_by_symbol([definer] + importers, "CoreClass", max_importers=0)
110
+
111
+ # Even with max_importers=0, the definer must be in the result
112
+ result_paths = {c.path for c in result}
113
+ assert "core.py" in result_paths
114
+ assert meta["definers_found"] == 1
115
+ assert meta["importers_returned"] == 0
116
+ assert meta["truncated"] is True
117
+
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # 4. agent_view suppressed flags metadata
121
+ # ---------------------------------------------------------------------------
122
+
123
+ def test_agent_view_suppressed_flags_detected() -> None:
124
+ """When metrics_summary.requested=True, agent_view includes it in suppressed_flags."""
125
+ sm = _make_sm(
126
+ metrics_summary=MetricsSummary(requested=True, file_count=5),
127
+ )
128
+ result = agent_view(sm)
129
+
130
+ assert "agent_mode" in result
131
+ am = result["agent_mode"]
132
+ assert "--full-metrics" in am.get("suppressed_flags", [])
133
+ assert am.get("suppressed_note") == "computed but excluded from agent_view"
134
+
135
+
136
+ def test_agent_view_suppressed_graph_modules() -> None:
137
+ """When module_graph.summary.requested=True, --graph-modules appears in suppressed_flags."""
138
+ mg = ModuleGraph(summary=ModuleGraphSummary(requested=True))
139
+ sm = _make_sm(module_graph=mg)
140
+ result = agent_view(sm)
141
+
142
+ am = result["agent_mode"]
143
+ assert "--graph-modules" in am.get("suppressed_flags", [])
144
+
145
+
146
+ def test_agent_view_suppressed_docs() -> None:
147
+ """When doc_summary.requested=True, --docs appears in suppressed_flags."""
148
+ sm = _make_sm(doc_summary=DocSummary(requested=True))
149
+ result = agent_view(sm)
150
+
151
+ am = result["agent_mode"]
152
+ assert "--docs" in am.get("suppressed_flags", [])
153
+
154
+
155
+ def test_agent_view_no_suppressed_flags() -> None:
156
+ """agent_view with no suppressed flags doesn't include suppressed_flags key."""
157
+ sm = _make_sm()
158
+ result = agent_view(sm)
159
+
160
+ assert "agent_mode" in result
161
+ am = result["agent_mode"]
162
+ assert "suppressed_flags" not in am
163
+ assert "--dependencies" in am.get("auto_enabled", [])
164
+
165
+
166
+ def test_agent_view_auto_enabled_always_present() -> None:
167
+ """auto_enabled list always contains the three flags agent mode enables."""
168
+ sm = _make_sm()
169
+ result = agent_view(sm)
170
+
171
+ am = result["agent_mode"]
172
+ assert set(am["auto_enabled"]) == {"--dependencies", "--env-map", "--code-notes"}
173
+
174
+
175
+ # ---------------------------------------------------------------------------
176
+ # 6 & 7. Empty entry point lists omitted from output
177
+ # ---------------------------------------------------------------------------
178
+
179
+ def test_agent_view_empty_ep_lists_omitted() -> None:
180
+ """When no dev/auxiliary entry points, those keys are absent from agent_view output."""
181
+ sm = _make_sm(entry_points=[])
182
+ result = agent_view(sm)
183
+
184
+ assert "development_entry_points" not in result
185
+ assert "auxiliary_entry_points" not in result
186
+ # production list still present (can be empty)
187
+ assert "entry_points" in result
188
+
189
+
190
+ def test_agent_view_dev_ep_present_when_nonempty() -> None:
191
+ """When a dev entry point exists, development_entry_points appears in agent_view."""
192
+ dev_ep = EntryPoint(
193
+ path="webpack.config.js",
194
+ stack="nodejs",
195
+ classification="development",
196
+ entrypoint_type="development",
197
+ runtime_relevance="low",
198
+ )
199
+ sm = _make_sm(entry_points=[dev_ep])
200
+ result = agent_view(sm)
201
+
202
+ assert "development_entry_points" in result
203
+
204
+
205
+ def test_standard_view_empty_ep_lists_omitted() -> None:
206
+ """When no dev/auxiliary entry points, those keys are absent from standard_view."""
207
+ sm = _make_sm(entry_points=[])
208
+ result = standard_view(sm)
209
+
210
+ assert "development_entry_points" not in result
211
+ assert "auxiliary_entry_points" not in result
212
+ assert "entry_points" in result
213
+
214
+
215
+ def test_standard_view_dev_ep_present_when_nonempty() -> None:
216
+ """When a dev entry point exists, it appears in standard_view."""
217
+ dev_ep = EntryPoint(
218
+ path="webpack.config.js",
219
+ stack="nodejs",
220
+ classification="development",
221
+ entrypoint_type="development",
222
+ runtime_relevance="low",
223
+ )
224
+ sm = _make_sm(entry_points=[dev_ep])
225
+ result = standard_view(sm)
226
+
227
+ assert "development_entry_points" in result
228
+
229
+
230
+ # ---------------------------------------------------------------------------
231
+ # 8. symbol_query in contract output
232
+ # ---------------------------------------------------------------------------
233
+
234
+ def test_symbol_query_in_contract_minimal_view() -> None:
235
+ """Truncation metadata appears in minimal contract output when truncated."""
236
+ trunc = {
237
+ "symbol": "MyClass",
238
+ "definers_found": 1,
239
+ "importers_found": 100,
240
+ "importers_returned": 50,
241
+ "references_found": 0,
242
+ "total_returned": 51,
243
+ "truncated": True,
244
+ "truncation_reason": "max_importers_limit",
245
+ "override_hint": "--symbol MyClass --max-importers 100",
246
+ }
247
+ cs = ContractSummary(
248
+ mode="contract",
249
+ total_files=200,
250
+ extracted_files=51,
251
+ symbol_truncation=trunc,
252
+ )
253
+ sm = _make_sm(contract_summary=cs, file_contracts=[])
254
+ result = contract_view(sm, depth="minimal")
255
+
256
+ assert "symbol_query" in result
257
+ sq = result["symbol_query"]
258
+ assert sq["truncated"] is True
259
+ assert sq["symbol"] == "MyClass"
260
+ assert sq["importers_found"] == 100
261
+
262
+
263
+ def test_symbol_query_absent_when_not_truncated() -> None:
264
+ """symbol_query is absent when symbol_truncation is None."""
265
+ cs = ContractSummary(
266
+ mode="contract",
267
+ total_files=10,
268
+ extracted_files=3,
269
+ symbol_truncation=None,
270
+ )
271
+ sm = _make_sm(contract_summary=cs, file_contracts=[])
272
+ result = contract_view(sm, depth="minimal")
273
+
274
+ assert "symbol_query" not in result
275
+
276
+
277
+ def test_symbol_query_in_contract_standard_view() -> None:
278
+ """Truncation metadata appears in standard contract output when truncated."""
279
+ trunc = {
280
+ "symbol": "Foo",
281
+ "definers_found": 1,
282
+ "importers_found": 75,
283
+ "importers_returned": 50,
284
+ "references_found": 0,
285
+ "total_returned": 51,
286
+ "truncated": True,
287
+ "truncation_reason": "max_importers_limit",
288
+ "override_hint": "--symbol Foo --max-importers 75",
289
+ }
290
+ cs = ContractSummary(
291
+ mode="standard",
292
+ total_files=150,
293
+ extracted_files=51,
294
+ symbol_truncation=trunc,
295
+ )
296
+ sm = _make_sm(contract_summary=cs, file_contracts=[])
297
+ result = contract_view(sm, depth="standard")
298
+
299
+ assert "symbol_query" in result
300
+ sq = result["symbol_query"]
301
+ assert sq["truncated"] is True
302
+ assert sq["symbol"] == "Foo"
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