sourcecode 0.44.0__tar.gz → 0.46.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 (150) hide show
  1. {sourcecode-0.44.0 → sourcecode-0.46.0}/PKG-INFO +1 -1
  2. {sourcecode-0.44.0 → sourcecode-0.46.0}/pyproject.toml +1 -1
  3. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/__init__.py +1 -1
  4. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/cli.py +89 -28
  5. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/contract_pipeline.py +3 -1
  6. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/heuristic.py +9 -1
  7. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/env_analyzer.py +2 -2
  8. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/prepare_context.py +38 -3
  9. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/serializer.py +36 -3
  10. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/workspace.py +35 -1
  11. {sourcecode-0.44.0 → sourcecode-0.46.0}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
  12. {sourcecode-0.44.0 → sourcecode-0.46.0}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
  13. {sourcecode-0.44.0 → sourcecode-0.46.0}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
  14. {sourcecode-0.44.0 → sourcecode-0.46.0}/.gitignore +0 -0
  15. {sourcecode-0.44.0 → sourcecode-0.46.0}/.ruff.toml +0 -0
  16. {sourcecode-0.44.0 → sourcecode-0.46.0}/CONTRIBUTING.md +0 -0
  17. {sourcecode-0.44.0 → sourcecode-0.46.0}/LICENSE +0 -0
  18. {sourcecode-0.44.0 → sourcecode-0.46.0}/README.md +0 -0
  19. {sourcecode-0.44.0 → sourcecode-0.46.0}/SECURITY.md +0 -0
  20. {sourcecode-0.44.0 → sourcecode-0.46.0}/docs/privacy.md +0 -0
  21. {sourcecode-0.44.0 → sourcecode-0.46.0}/docs/schema.md +0 -0
  22. {sourcecode-0.44.0 → sourcecode-0.46.0}/raw +0 -0
  23. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/adaptive_scanner.py +0 -0
  24. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/architecture_analyzer.py +0 -0
  25. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/architecture_summary.py +0 -0
  26. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/ast_extractor.py +0 -0
  27. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/classifier.py +0 -0
  28. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  29. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/confidence_analyzer.py +0 -0
  30. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/context_scorer.py +0 -0
  31. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/context_summarizer.py +0 -0
  32. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/contract_model.py +0 -0
  33. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/coverage_parser.py +0 -0
  34. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/dependency_analyzer.py +0 -0
  35. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/__init__.py +0 -0
  36. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/base.py +0 -0
  37. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  38. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/dart.py +0 -0
  39. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/dotnet.py +0 -0
  40. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/elixir.py +0 -0
  41. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/go.py +0 -0
  42. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/hybrid.py +0 -0
  43. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/java.py +0 -0
  44. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  45. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/nodejs.py +0 -0
  46. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/parsers.py +0 -0
  47. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/php.py +0 -0
  48. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/project.py +0 -0
  49. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/python.py +0 -0
  50. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/ruby.py +0 -0
  51. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/rust.py +0 -0
  52. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/systems.py +0 -0
  53. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/terraform.py +0 -0
  54. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/detectors/tooling.py +0 -0
  55. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/doc_analyzer.py +0 -0
  56. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  57. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/file_classifier.py +0 -0
  58. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/git_analyzer.py +0 -0
  59. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/graph_analyzer.py +0 -0
  60. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/metrics_analyzer.py +0 -0
  61. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/ranking_engine.py +0 -0
  62. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/redactor.py +0 -0
  63. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/relevance_scorer.py +0 -0
  64. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/repo_classifier.py +0 -0
  65. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/runtime_classifier.py +0 -0
  66. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/scanner.py +0 -0
  67. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/schema.py +0 -0
  68. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/semantic_analyzer.py +0 -0
  69. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/summarizer.py +0 -0
  70. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/telemetry/__init__.py +0 -0
  71. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/telemetry/config.py +0 -0
  72. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/telemetry/consent.py +0 -0
  73. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/telemetry/events.py +0 -0
  74. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/telemetry/filters.py +0 -0
  75. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/telemetry/transport.py +0 -0
  76. {sourcecode-0.44.0 → sourcecode-0.46.0}/src/sourcecode/tree_utils.py +0 -0
  77. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/__init__.py +0 -0
  78. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/conftest.py +0 -0
  79. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/fixtures/coverage.xml +0 -0
  80. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  81. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  82. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  83. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/fixtures/go_service/go.mod +0 -0
  84. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/fixtures/jacoco.xml +0 -0
  85. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/fixtures/lcov.info +0 -0
  86. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  87. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/fixtures/nextjs_app/package.json +0 -0
  88. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  89. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  90. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  91. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  92. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  93. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  94. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_architecture_analyzer.py +0 -0
  95. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_architecture_summary.py +0 -0
  96. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_ast_extractor.py +0 -0
  97. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_block1_reliability.py +0 -0
  98. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_block2_coverage.py +0 -0
  99. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_block5_quality.py +0 -0
  100. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_classifier.py +0 -0
  101. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_cli.py +0 -0
  102. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_code_notes_analyzer.py +0 -0
  103. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_context_scorer.py +0 -0
  104. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_contract_pipeline.py +0 -0
  105. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_coverage_parser.py +0 -0
  106. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_cross_consistency.py +0 -0
  107. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_dependency_analyzer_node_python.py +0 -0
  108. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  109. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_dependency_schema.py +0 -0
  110. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_detector_dotnet.py +0 -0
  111. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_detector_go_rust_java.py +0 -0
  112. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_detector_nodejs.py +0 -0
  113. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_detector_php_ruby_dart.py +0 -0
  114. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_detector_python.py +0 -0
  115. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_detector_universal_managed.py +0 -0
  116. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_detector_universal_systems.py +0 -0
  117. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_detectors_base.py +0 -0
  118. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  119. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_doc_analyzer_python.py +0 -0
  120. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  121. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_graph_analyzer_python_node.py +0 -0
  122. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_graph_schema.py +0 -0
  123. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_hybrid_inference.py +0 -0
  124. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_integration.py +0 -0
  125. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_integration_dependencies.py +0 -0
  126. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_integration_detection.py +0 -0
  127. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_integration_docs.py +0 -0
  128. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_integration_graph_modules.py +0 -0
  129. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_integration_lqn.py +0 -0
  130. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_integration_metrics.py +0 -0
  131. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_integration_multistack.py +0 -0
  132. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_integration_semantics.py +0 -0
  133. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_integration_universal.py +0 -0
  134. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_metrics_analyzer.py +0 -0
  135. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_packaging.py +0 -0
  136. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_phase1_improvements.py +0 -0
  137. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_pipeline_integrity.py +0 -0
  138. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_real_projects.py +0 -0
  139. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_redactor.py +0 -0
  140. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_scanner.py +0 -0
  141. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_schema.py +0 -0
  142. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_schema_normalization.py +0 -0
  143. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_semantic_analyzer_node.py +0 -0
  144. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_semantic_analyzer_python.py +0 -0
  145. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_semantic_import_resolution.py +0 -0
  146. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_semantic_schema.py +0 -0
  147. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_signal_hierarchy.py +0 -0
  148. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_summarizer.py +0 -0
  149. {sourcecode-0.44.0 → sourcecode-0.46.0}/tests/test_telemetry.py +0 -0
  150. {sourcecode-0.44.0 → sourcecode-0.46.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.44.0
3
+ Version: 0.46.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.44.0"
7
+ version = "0.46.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.44.0"
3
+ __version__ = "0.46.0"
@@ -660,6 +660,21 @@ def main(
660
660
  )
661
661
  raise typer.Exit(code=1)
662
662
 
663
+ if symbol and mode not in ("contract", "standard"):
664
+ typer.echo(
665
+ f"Error: --symbol requires --mode contract or standard (got '{mode}'). "
666
+ "Symbol search uses the contract pipeline which does not run in raw mode.",
667
+ err=True,
668
+ )
669
+ raise typer.Exit(code=1)
670
+
671
+ if entrypoints_only and mode not in ("contract", "standard"):
672
+ typer.echo(
673
+ f"Error: --entrypoints-only requires --mode contract or standard (got '{mode}').",
674
+ err=True,
675
+ )
676
+ raise typer.Exit(code=1)
677
+
663
678
  if dependency_depth > 0:
664
679
  typer.echo(
665
680
  f"[warning] --dependency-depth {dependency_depth} has no effect: "
@@ -716,8 +731,11 @@ def main(
716
731
  # These flags produce standard_view-only output sections not in contract_view.
717
732
  # Preserves backward compat: callers using any legacy flag get their previous format.
718
733
  # New callers opt into contract mode via --mode contract (or bare invocation).
734
+ # Legacy flags that produce output sections incompatible with contract_view
735
+ # force mode to raw. --agent is excluded: it now runs the contract pipeline
736
+ # and enriches contract_view with auto-enabled analyzers (deps, env, notes).
719
737
  _legacy_flags_active = (
720
- compact or agent or tree or format == "yaml" or trace_pipeline
738
+ compact or tree or format == "yaml" or trace_pipeline
721
739
  or docs or semantics or graph_modules or full_metrics or architecture
722
740
  )
723
741
  if mode in ("contract", "standard") and _legacy_flags_active:
@@ -1257,7 +1275,13 @@ def main(
1257
1275
  eco_order = 0 if d.ecosystem == primary_ecosystem else 1
1258
1276
  return (role_order, eco_order, d.name.lower())
1259
1277
 
1260
- sm.key_dependencies = sorted(direct_deps, key=_dep_sort_key)[:15]
1278
+ _seen_dep_names: set[str] = set()
1279
+ _deduped_deps: list[Any] = []
1280
+ for d in sorted(direct_deps, key=_dep_sort_key):
1281
+ if d.name not in _seen_dep_names:
1282
+ _seen_dep_names.add(d.name)
1283
+ _deduped_deps.append(d)
1284
+ sm.key_dependencies = _deduped_deps[:15]
1261
1285
 
1262
1286
  # LQN-02: deterministic NL summary
1263
1287
  sm.project_summary = ProjectSummarizer(target).generate(sm)
@@ -1356,37 +1380,61 @@ def main(
1356
1380
  _is_contract_mode = mode in ("contract", "standard")
1357
1381
  if _is_contract_mode:
1358
1382
  from sourcecode.contract_pipeline import ContractPipeline
1383
+ from sourcecode.contract_model import ContractSummary as _ContractSummary
1359
1384
  _cp = ContractPipeline()
1360
- _contracts, _contract_summary = _cp.run(
1361
- target,
1362
- sm.file_paths,
1363
- entry_points=sm.entry_points,
1364
- monorepo_packages=sm.monorepo_packages,
1365
- mode=mode,
1366
- rank_by=rank_by, # type: ignore[arg-type]
1367
- max_symbols=max_symbols,
1368
- dependency_depth=dependency_depth,
1369
- entrypoints_only=entrypoints_only,
1370
- changed_only=changed_only,
1371
- symbol=symbol,
1372
- compress_types=compress_types,
1373
- max_importers=max_importers,
1374
- semantic_calls=sm.semantic_calls or None,
1375
- code_notes=sm.code_notes or None,
1376
- )
1385
+ try:
1386
+ _contracts, _contract_summary = _cp.run(
1387
+ target,
1388
+ sm.file_paths,
1389
+ entry_points=sm.entry_points,
1390
+ monorepo_packages=sm.monorepo_packages,
1391
+ mode=mode,
1392
+ rank_by=rank_by, # type: ignore[arg-type]
1393
+ max_symbols=max_symbols,
1394
+ dependency_depth=dependency_depth,
1395
+ entrypoints_only=entrypoints_only,
1396
+ changed_only=changed_only,
1397
+ symbol=symbol,
1398
+ compress_types=compress_types,
1399
+ max_importers=max_importers,
1400
+ semantic_calls=sm.semantic_calls or None,
1401
+ code_notes=sm.code_notes or None,
1402
+ )
1403
+ except Exception as _exc:
1404
+ typer.echo(f"[error] contract pipeline failed: {_exc}", err=True)
1405
+ _contracts = []
1406
+ _contract_summary = _ContractSummary(
1407
+ mode=mode,
1408
+ total_files=0,
1409
+ extracted_files=0,
1410
+ filtered_files=0,
1411
+ method_breakdown={},
1412
+ ranked_by=rank_by,
1413
+ limitations=[f"pipeline_error: {type(_exc).__name__}"],
1414
+ )
1377
1415
  sm = _replace(sm, file_contracts=_contracts, contract_summary=_contract_summary)
1378
1416
  if symbol is not None and len(_contracts) == 0:
1379
- typer.echo(
1380
- f"[warning] --symbol '{symbol}' matched 0 files. "
1381
- "The symbol may not exist at the current --depth, or the name may differ in case. "
1382
- "Try --depth 8 or verify the symbol name.",
1383
- err=True,
1384
- )
1385
- if agent:
1386
- typer.echo(f"[contract] {len(_contracts)} files extracted ({_contract_summary.method_breakdown})", err=True)
1417
+ _jvm_stacks = {"java", "kotlin", "scala", "groovy"}
1418
+ _is_jvm_repo = any(s.stack in _jvm_stacks for s in sm.stacks)
1419
+ if _is_jvm_repo:
1420
+ typer.echo(
1421
+ f"[warning] --symbol '{symbol}' matched 0 files. "
1422
+ "Per-file AST extraction is not available for Java/JVM repos — "
1423
+ "symbol search works only with Python, TypeScript, and JavaScript. "
1424
+ "Use --git-context or --code-notes for JVM navigation.",
1425
+ err=True,
1426
+ )
1427
+ else:
1428
+ typer.echo(
1429
+ f"[warning] --symbol '{symbol}' matched 0 files. "
1430
+ "The symbol may not exist, the name may differ in case, "
1431
+ "or the file may be outside the scanned depth. "
1432
+ "Try --depth 8 or verify the symbol name.",
1433
+ err=True,
1434
+ )
1387
1435
 
1388
1436
  # 4. Serialize
1389
- if _is_contract_mode:
1437
+ if _is_contract_mode and not agent:
1390
1438
  from sourcecode.serializer import contract_view as _contract_view
1391
1439
  _depth = _CONTRACT_DEPTH.get(mode, "minimal")
1392
1440
  data = _contract_view(sm, emit_graph=emit_graph, depth=_depth)
@@ -1395,6 +1443,19 @@ def main(
1395
1443
  content = json.dumps(data, indent=2, ensure_ascii=False)
1396
1444
  elif agent:
1397
1445
  data = agent_view(sm)
1446
+ # When contract pipeline ran (mode=contract, no legacy flags), include
1447
+ # per-file contracts in agent output so agents get structural context.
1448
+ if _is_contract_mode and sm.file_contracts:
1449
+ from sourcecode.serializer import _serialize_contract_minimal
1450
+ data["contracts"] = [_serialize_contract_minimal(c) for c in sm.file_contracts]
1451
+ if sm.contract_summary is not None:
1452
+ cs = sm.contract_summary
1453
+ data["contract_summary"] = {
1454
+ "files": cs.extracted_files,
1455
+ "total": cs.total_files,
1456
+ }
1457
+ if cs.method_breakdown:
1458
+ data["contract_summary"]["methods"] = cs.method_breakdown
1398
1459
  if not no_redact:
1399
1460
  data = redact_dict(data)
1400
1461
  content = json.dumps(data, indent=2, ensure_ascii=False)
@@ -241,7 +241,9 @@ class ContractPipeline:
241
241
  contracts.append(contract)
242
242
  method_counts[contract.extraction_method] += 1
243
243
 
244
- if not self._extractor.has_tree_sitter():
244
+ _js_ts_languages = {"typescript", "javascript", "tsx", "jsx"}
245
+ _has_js_ts = any(c.language in _js_ts_languages for c in contracts)
246
+ if _has_js_ts and not self._extractor.has_tree_sitter():
245
247
  limitations.append(
246
248
  "tree_sitter_unavailable: JS/TS extraction uses heuristics. "
247
249
  "Install with: pip install 'sourcecode[ast]'"
@@ -66,6 +66,13 @@ class HeuristicDetector(AbstractDetector):
66
66
  counts[stack] += 1
67
67
  break
68
68
 
69
+ # Suppress minority stacks: if a language appears in fewer than 3 files
70
+ # AND represents less than 10% of detected source files, it is likely
71
+ # noise (stray config files, vendored snippets) rather than a real stack.
72
+ # Always emit the dominant language regardless of absolute count.
73
+ total_detected = sum(counts.values())
74
+ _ABS_MIN = 3
75
+ _REL_MIN = 0.10
69
76
  stacks = [
70
77
  StackDetection(
71
78
  stack=stack,
@@ -73,7 +80,8 @@ class HeuristicDetector(AbstractDetector):
73
80
  confidence="low",
74
81
  manifests=[],
75
82
  )
76
- for stack, _count in counts.most_common()
83
+ for stack, count in counts.most_common()
84
+ if count >= _ABS_MIN or (total_detected > 0 and count / total_detected >= _REL_MIN)
77
85
  ]
78
86
 
79
87
  entry_points: list[EntryPoint] = []
@@ -11,8 +11,8 @@ _MAX_FILE_SIZE = 512 * 1024 # 512 KB
11
11
 
12
12
  _SKIP_DIRS = {
13
13
  "node_modules", ".git", "__pycache__", ".venv", "venv",
14
- ".mypy_cache", "dist", "build", ".tox", ".eggs", "coverage",
15
- ".next", ".nuxt", ".output", "vendor",
14
+ ".mypy_cache", "dist", "build", "target", ".gradle",
15
+ ".tox", ".eggs", "coverage", ".next", ".nuxt", ".output", "vendor",
16
16
  }
17
17
 
18
18
  _CODE_EXTENSIONS = {
@@ -349,21 +349,56 @@ class TaskContextBuilder:
349
349
  spec = TASKS[task_name]
350
350
 
351
351
  # ── 1. Scan ────────────────────────────────────────────────────────
352
- from sourcecode.scanner import FileScanner
352
+ from sourcecode.adaptive_scanner import AdaptiveScanner
353
+ from sourcecode.repo_classifier import RepoClassifier
353
354
  from sourcecode.tree_utils import flatten_file_tree
354
355
 
355
- scanner = FileScanner(self.root, max_depth=6)
356
+ _topology = RepoClassifier().classify(self.root)
357
+ scanner = AdaptiveScanner(self.root, topology=_topology, base_depth=6)
356
358
  file_tree = scanner.scan_tree()
357
359
  manifests = scanner.find_manifests()
358
360
  all_paths = [p.replace("\\", "/") for p in flatten_file_tree(file_tree)]
359
361
 
360
362
  # ── 2. Detect stacks + entry points ───────────────────────────────
363
+ from dataclasses import replace as _replace
361
364
  from sourcecode.detectors import ProjectDetector, build_default_detectors
362
365
  from sourcecode.workspace import WorkspaceAnalyzer
363
366
 
364
367
  detector = ProjectDetector(build_default_detectors())
365
368
  workspace_analysis = WorkspaceAnalyzer().analyze(self.root, manifests)
366
- stacks, entry_points, _ = detector.detect(self.root, file_tree, manifests)
369
+
370
+ _root_manifests = [
371
+ m for m in manifests
372
+ if Path(m).resolve().parent == self.root
373
+ ]
374
+ _detection_manifests = _root_manifests if workspace_analysis.workspaces else manifests
375
+ if workspace_analysis.is_monorepo and not _root_manifests:
376
+ from sourcecode.schema import EntryPoint, StackDetection
377
+ stacks: list[StackDetection] = []
378
+ entry_points: list[EntryPoint] = []
379
+ else:
380
+ stacks, entry_points, _ = detector.detect(self.root, file_tree, _detection_manifests)
381
+
382
+ # Iterate workspaces to collect per-workspace stacks and entry points —
383
+ # same approach as the main CLI (cli.py lines 971-1041).
384
+ for workspace in workspace_analysis.workspaces:
385
+ ws_root = self.root / workspace.path
386
+ if not ws_root.exists() or not ws_root.is_dir():
387
+ continue
388
+ _ws_topology = RepoClassifier().classify(ws_root)
389
+ _ws_scanner = AdaptiveScanner(ws_root, topology=_ws_topology, base_depth=6)
390
+ _ws_tree = _ws_scanner.scan_tree()
391
+ _ws_manifests = _ws_scanner.find_manifests()
392
+ _ws_stacks, _ws_eps, _ = detector.detect(ws_root, _ws_tree, _ws_manifests)
393
+ stacks.extend(
394
+ _replace(s, root=workspace.path, workspace=workspace.path, primary=False)
395
+ for s in _ws_stacks
396
+ )
397
+ entry_points.extend(
398
+ _replace(ep, path=f"{workspace.path}/{ep.path}")
399
+ for ep in _ws_eps
400
+ )
401
+
367
402
  stacks, project_type = detector.classify_results(
368
403
  file_tree, stacks, entry_points,
369
404
  project_type_override="monorepo" if workspace_analysis.is_monorepo else None,
@@ -121,10 +121,16 @@ def _dependency_groups(sm: SourceMap) -> dict[str, list[dict[str, Any]]]:
121
121
 
122
122
  if role in _PRODUCTION_DEP_ROLES and scope not in {"dev"}:
123
123
  groups["production_dependencies"].append(item)
124
+ _jvm_ecosystems = {"maven", "gradle", "java", "kotlin", "scala", "groovy"}
124
125
  if dep.source == "manifest" and name_key not in import_index:
125
- suspect = dict(item)
126
- suspect["reason"] = "declared as production dependency but no static import observed"
127
- groups["suspicious_dependencies"].append(suspect)
126
+ if dep.ecosystem in _jvm_ecosystems:
127
+ # Static import check unsupported for JVM: import index only covers
128
+ # Python/JS/TS. Flagging JVM deps as suspicious produces only false positives.
129
+ pass
130
+ else:
131
+ suspect = dict(item)
132
+ suspect["reason"] = "declared as production dependency but no static import observed"
133
+ groups["suspicious_dependencies"].append(suspect)
128
134
  elif role in _TEST_DEP_ROLES:
129
135
  groups["test_utilities"].append(item)
130
136
  elif role in _BUILD_DEP_ROLES:
@@ -1168,6 +1174,33 @@ def _contract_view_minimal(
1168
1174
  if cs.symbol_truncation:
1169
1175
  result["symbol_query"] = cs.symbol_truncation
1170
1176
 
1177
+ # Monorepo package roles — helps agents understand workspace structure
1178
+ if sm.monorepo_packages:
1179
+ _noise_roles = {"benchmark_layer", "tooling_layer", "docs_layer", "test_layer"}
1180
+ operational_pkgs = [
1181
+ {"path": p.path, "role": p.architectural_role, "criticality": p.criticality}
1182
+ for p in sm.monorepo_packages
1183
+ if p.architectural_role not in _noise_roles
1184
+ ]
1185
+ if operational_pkgs:
1186
+ result["workspace_packages"] = operational_pkgs
1187
+
1188
+ # Confidence summary — detection quality signal
1189
+ if sm.confidence_summary is not None:
1190
+ cs_conf = sm.confidence_summary
1191
+ conf: dict[str, Any] = {
1192
+ "overall": cs_conf.overall,
1193
+ "stack": cs_conf.stack_confidence,
1194
+ "entry_points": cs_conf.entry_point_confidence,
1195
+ }
1196
+ if cs_conf.anomalies:
1197
+ conf["anomalies"] = cs_conf.anomalies
1198
+ result["confidence"] = conf
1199
+
1200
+ # Analysis gaps — explicit about what could not be analyzed
1201
+ if sm.analysis_gaps:
1202
+ result["analysis_gaps"] = [asdict(g) for g in sm.analysis_gaps]
1203
+
1171
1204
  return result
1172
1205
 
1173
1206
 
@@ -45,7 +45,7 @@ class WorkspaceAnalyzer:
45
45
 
46
46
  def _detect_markers(self, root: Path) -> list[str]:
47
47
  markers: list[str] = []
48
- for filename in ("pnpm-workspace.yaml", "go.work", "turbo.json", "lerna.json"):
48
+ for filename in ("pnpm-workspace.yaml", "go.work", "turbo.json", "lerna.json", "nx.json", "rush.json"):
49
49
  if (root / filename).exists():
50
50
  markers.append(filename)
51
51
 
@@ -54,6 +54,17 @@ class WorkspaceAnalyzer:
54
54
  content = cargo.read_text(encoding="utf-8", errors="replace")
55
55
  if "[workspace]" in content:
56
56
  markers.append("Cargo.toml[workspace]")
57
+
58
+ pkg = root / "package.json"
59
+ if pkg.exists():
60
+ try:
61
+ import json as _json
62
+ data = _json.loads(pkg.read_text(encoding="utf-8", errors="replace"))
63
+ if isinstance(data, dict) and data.get("workspaces"):
64
+ markers.append("package.json[workspaces]")
65
+ except Exception:
66
+ pass
67
+
57
68
  return markers
58
69
 
59
70
  def _workspace_candidates_from_manifests(
@@ -84,6 +95,8 @@ class WorkspaceAnalyzer:
84
95
  candidates.extend(self._from_go_work(root))
85
96
  if "Cargo.toml[workspace]" in markers:
86
97
  candidates.extend(self._from_cargo_workspace(root))
98
+ if "package.json[workspaces]" in markers:
99
+ candidates.extend(self._from_npm_workspaces(root))
87
100
  return [candidate for candidate in candidates if self._is_allowed_workspace(candidate.path)]
88
101
 
89
102
  def _from_pnpm_workspace(self, root: Path) -> list[WorkspaceCandidate]:
@@ -143,6 +156,27 @@ class WorkspaceAnalyzer:
143
156
  in_members = False
144
157
  return candidates
145
158
 
159
+ def _from_npm_workspaces(self, root: Path) -> list[WorkspaceCandidate]:
160
+ try:
161
+ import json as _json
162
+ data = _json.loads((root / "package.json").read_text(encoding="utf-8", errors="replace"))
163
+ except Exception:
164
+ return []
165
+ workspaces = data.get("workspaces", [])
166
+ if isinstance(workspaces, dict):
167
+ workspaces = workspaces.get("packages", [])
168
+ if not isinstance(workspaces, list):
169
+ return []
170
+ candidates: list[WorkspaceCandidate] = []
171
+ for pattern in workspaces:
172
+ if not isinstance(pattern, str):
173
+ continue
174
+ for path in self._resolve_pattern(root, pattern):
175
+ candidates.append(
176
+ WorkspaceCandidate(path=path, reason="marker:package.json[workspaces]", depth=len(Path(path).parts))
177
+ )
178
+ return candidates
179
+
146
180
  def _resolve_pattern(self, root: Path, pattern: str) -> list[str]:
147
181
  matches: list[str] = []
148
182
  for candidate in root.glob(pattern):
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