sourcecode 0.45.0__tar.gz → 0.47.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.45.0 → sourcecode-0.47.0}/PKG-INFO +1 -1
  2. {sourcecode-0.45.0 → sourcecode-0.47.0}/pyproject.toml +1 -1
  3. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/__init__.py +1 -1
  4. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/cli.py +18 -4
  5. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/prepare_context.py +38 -3
  6. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/serializer.py +91 -33
  7. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/workspace.py +35 -1
  8. {sourcecode-0.45.0 → sourcecode-0.47.0}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
  9. {sourcecode-0.45.0 → sourcecode-0.47.0}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
  10. {sourcecode-0.45.0 → sourcecode-0.47.0}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
  11. {sourcecode-0.45.0 → sourcecode-0.47.0}/.gitignore +0 -0
  12. {sourcecode-0.45.0 → sourcecode-0.47.0}/.ruff.toml +0 -0
  13. {sourcecode-0.45.0 → sourcecode-0.47.0}/CONTRIBUTING.md +0 -0
  14. {sourcecode-0.45.0 → sourcecode-0.47.0}/LICENSE +0 -0
  15. {sourcecode-0.45.0 → sourcecode-0.47.0}/README.md +0 -0
  16. {sourcecode-0.45.0 → sourcecode-0.47.0}/SECURITY.md +0 -0
  17. {sourcecode-0.45.0 → sourcecode-0.47.0}/docs/privacy.md +0 -0
  18. {sourcecode-0.45.0 → sourcecode-0.47.0}/docs/schema.md +0 -0
  19. {sourcecode-0.45.0 → sourcecode-0.47.0}/raw +0 -0
  20. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/adaptive_scanner.py +0 -0
  21. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/architecture_analyzer.py +0 -0
  22. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/architecture_summary.py +0 -0
  23. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/ast_extractor.py +0 -0
  24. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/classifier.py +0 -0
  25. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  26. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/confidence_analyzer.py +0 -0
  27. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/context_scorer.py +0 -0
  28. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/context_summarizer.py +0 -0
  29. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/contract_model.py +0 -0
  30. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/contract_pipeline.py +0 -0
  31. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/coverage_parser.py +0 -0
  32. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/dependency_analyzer.py +0 -0
  33. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/__init__.py +0 -0
  34. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/base.py +0 -0
  35. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  36. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/dart.py +0 -0
  37. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/dotnet.py +0 -0
  38. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/elixir.py +0 -0
  39. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/go.py +0 -0
  40. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/heuristic.py +0 -0
  41. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/hybrid.py +0 -0
  42. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/java.py +0 -0
  43. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  44. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/nodejs.py +0 -0
  45. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/parsers.py +0 -0
  46. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/php.py +0 -0
  47. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/project.py +0 -0
  48. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/python.py +0 -0
  49. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/ruby.py +0 -0
  50. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/rust.py +0 -0
  51. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/systems.py +0 -0
  52. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/terraform.py +0 -0
  53. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/detectors/tooling.py +0 -0
  54. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/doc_analyzer.py +0 -0
  55. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  56. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/env_analyzer.py +0 -0
  57. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/file_classifier.py +0 -0
  58. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/git_analyzer.py +0 -0
  59. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/graph_analyzer.py +0 -0
  60. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/metrics_analyzer.py +0 -0
  61. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/ranking_engine.py +0 -0
  62. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/redactor.py +0 -0
  63. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/relevance_scorer.py +0 -0
  64. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/repo_classifier.py +0 -0
  65. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/runtime_classifier.py +0 -0
  66. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/scanner.py +0 -0
  67. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/schema.py +0 -0
  68. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/semantic_analyzer.py +0 -0
  69. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/summarizer.py +0 -0
  70. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/telemetry/__init__.py +0 -0
  71. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/telemetry/config.py +0 -0
  72. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/telemetry/consent.py +0 -0
  73. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/telemetry/events.py +0 -0
  74. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/telemetry/filters.py +0 -0
  75. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/telemetry/transport.py +0 -0
  76. {sourcecode-0.45.0 → sourcecode-0.47.0}/src/sourcecode/tree_utils.py +0 -0
  77. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/__init__.py +0 -0
  78. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/conftest.py +0 -0
  79. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/fixtures/coverage.xml +0 -0
  80. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  81. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  82. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  83. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/fixtures/go_service/go.mod +0 -0
  84. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/fixtures/jacoco.xml +0 -0
  85. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/fixtures/lcov.info +0 -0
  86. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  87. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/fixtures/nextjs_app/package.json +0 -0
  88. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  89. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  90. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  91. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  92. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  93. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  94. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_architecture_analyzer.py +0 -0
  95. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_architecture_summary.py +0 -0
  96. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_ast_extractor.py +0 -0
  97. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_block1_reliability.py +0 -0
  98. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_block2_coverage.py +0 -0
  99. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_block5_quality.py +0 -0
  100. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_classifier.py +0 -0
  101. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_cli.py +0 -0
  102. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_code_notes_analyzer.py +0 -0
  103. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_context_scorer.py +0 -0
  104. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_contract_pipeline.py +0 -0
  105. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_coverage_parser.py +0 -0
  106. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_cross_consistency.py +0 -0
  107. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_dependency_analyzer_node_python.py +0 -0
  108. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  109. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_dependency_schema.py +0 -0
  110. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_detector_dotnet.py +0 -0
  111. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_detector_go_rust_java.py +0 -0
  112. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_detector_nodejs.py +0 -0
  113. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_detector_php_ruby_dart.py +0 -0
  114. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_detector_python.py +0 -0
  115. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_detector_universal_managed.py +0 -0
  116. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_detector_universal_systems.py +0 -0
  117. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_detectors_base.py +0 -0
  118. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  119. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_doc_analyzer_python.py +0 -0
  120. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  121. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_graph_analyzer_python_node.py +0 -0
  122. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_graph_schema.py +0 -0
  123. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_hybrid_inference.py +0 -0
  124. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_integration.py +0 -0
  125. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_integration_dependencies.py +0 -0
  126. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_integration_detection.py +0 -0
  127. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_integration_docs.py +0 -0
  128. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_integration_graph_modules.py +0 -0
  129. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_integration_lqn.py +0 -0
  130. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_integration_metrics.py +0 -0
  131. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_integration_multistack.py +0 -0
  132. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_integration_semantics.py +0 -0
  133. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_integration_universal.py +0 -0
  134. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_metrics_analyzer.py +0 -0
  135. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_packaging.py +0 -0
  136. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_phase1_improvements.py +0 -0
  137. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_pipeline_integrity.py +0 -0
  138. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_real_projects.py +0 -0
  139. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_redactor.py +0 -0
  140. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_scanner.py +0 -0
  141. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_schema.py +0 -0
  142. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_schema_normalization.py +0 -0
  143. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_semantic_analyzer_node.py +0 -0
  144. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_semantic_analyzer_python.py +0 -0
  145. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_semantic_import_resolution.py +0 -0
  146. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_semantic_schema.py +0 -0
  147. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_signal_hierarchy.py +0 -0
  148. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_summarizer.py +0 -0
  149. {sourcecode-0.45.0 → sourcecode-0.47.0}/tests/test_telemetry.py +0 -0
  150. {sourcecode-0.45.0 → sourcecode-0.47.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.45.0
3
+ Version: 0.47.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.45.0"
7
+ version = "0.47.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.45.0"
3
+ __version__ = "0.47.0"
@@ -731,8 +731,11 @@ def main(
731
731
  # These flags produce standard_view-only output sections not in contract_view.
732
732
  # Preserves backward compat: callers using any legacy flag get their previous format.
733
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).
734
737
  _legacy_flags_active = (
735
- compact or agent or tree or format == "yaml" or trace_pipeline
738
+ compact or tree or format == "yaml" or trace_pipeline
736
739
  or docs or semantics or graph_modules or full_metrics or architecture
737
740
  )
738
741
  if mode in ("contract", "standard") and _legacy_flags_active:
@@ -1429,11 +1432,9 @@ def main(
1429
1432
  "Try --depth 8 or verify the symbol name.",
1430
1433
  err=True,
1431
1434
  )
1432
- if agent:
1433
- typer.echo(f"[contract] {len(_contracts)} files extracted ({_contract_summary.method_breakdown})", err=True)
1434
1435
 
1435
1436
  # 4. Serialize
1436
- if _is_contract_mode:
1437
+ if _is_contract_mode and not agent:
1437
1438
  from sourcecode.serializer import contract_view as _contract_view
1438
1439
  _depth = _CONTRACT_DEPTH.get(mode, "minimal")
1439
1440
  data = _contract_view(sm, emit_graph=emit_graph, depth=_depth)
@@ -1442,6 +1443,19 @@ def main(
1442
1443
  content = json.dumps(data, indent=2, ensure_ascii=False)
1443
1444
  elif agent:
1444
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
1445
1459
  if not no_redact:
1446
1460
  data = redact_dict(data)
1447
1461
  content = json.dumps(data, indent=2, ensure_ascii=False)
@@ -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,
@@ -24,6 +24,21 @@ from sourcecode.schema import (
24
24
  SourceMap,
25
25
  )
26
26
 
27
+ # ---------------------------------------------------------------------------
28
+ # Visibility caps — public output is intentionally small.
29
+ # Internal analysis stays broad; only high-signal results reach the output.
30
+ # ---------------------------------------------------------------------------
31
+ _EP_PRODUCTION_CAP = 5 # max production entry points in default output
32
+ _EP_DEV_CAP = 3 # max development entry points in default output
33
+ _FILE_RELEVANCE_LIMIT = 10 # max files in file_relevance section
34
+ _FILE_RELEVANCE_MIN_COMBINED = 0.20 # minimum combined score to appear (out of ~2.0 max)
35
+ _PROD_DEPS_CAP = 10 # max production dependencies shown
36
+ _SECONDARY_DEPS_CAP = 5 # max per dev/test/build dependency group
37
+ _MONOREPO_PKGS_CAP = 8 # max workspace/runtime packages shown
38
+ _KEY_DEPS_CAP = 10 # max key dependencies shown
39
+ _CODE_NOTES_CAP = 15 # max code notes in default output
40
+ _ENV_MAP_CAP = 15 # max env var entries in default output
41
+
27
42
 
28
43
  def to_json(sm: SourceMap | dict[str, Any], indent: int = 2) -> str:
29
44
  """Serialize SourceMap or dict to canonical JSON.
@@ -176,7 +191,7 @@ def _dep_import_key(name: str) -> str:
176
191
  return lowered.split("/")[0].replace("_", "-")
177
192
 
178
193
 
179
- def _file_relevance(sm: SourceMap, *, limit: int = 15) -> list[dict[str, Any]]:
194
+ def _file_relevance(sm: SourceMap, *, limit: int = _FILE_RELEVANCE_LIMIT) -> list[dict[str, Any]]:
180
195
  from sourcecode.ranking_engine import RankingEngine
181
196
 
182
197
  root = Path(sm.metadata.analyzed_path) if sm.metadata.analyzed_path else Path(".")
@@ -227,7 +242,20 @@ def _file_relevance(sm: SourceMap, *, limit: int = 15) -> list[dict[str, Any]]:
227
242
  sem_hub = semantic_hub_scores.get(path, 0.0) * 0.30
228
243
  combined = fs.score + content_rel + sem_hub
229
244
 
230
- if combined <= 0 and not (file_class and file_class.relevance > 0.3):
245
+ # Visibility threshold: require meaningful combined signal.
246
+ # Exception: high/medium-confidence files with strong content relevance
247
+ # can survive even if structural score is weak.
248
+ if combined < _FILE_RELEVANCE_MIN_COMBINED:
249
+ if not (file_class
250
+ and file_class.relevance > 0.45
251
+ and file_class.confidence in {"high", "medium"}):
252
+ continue
253
+
254
+ # Suppress low-confidence auxiliary/config files unless structurally prominent
255
+ if (file_class
256
+ and file_class.confidence == "low"
257
+ and file_class.category in {"config", "auxiliary"}
258
+ and combined < 0.45):
231
259
  continue
232
260
 
233
261
  item: dict[str, Any] = {
@@ -343,7 +371,7 @@ def compact_view(sm: SourceMap, *, no_tree: bool = False) -> dict[str, Any]:
343
371
  key_deps = [
344
372
  asdict(d) for d in sm.key_dependencies
345
373
  if (d.role or "unknown") in _PRODUCTION_DEP_ROLES and d.scope not in {"dev"}
346
- ]
374
+ ][:_KEY_DEPS_CAP]
347
375
  elif sm.dependency_summary is None or not sm.dependency_summary.requested:
348
376
  dep_summary_dict = None # "not analyzed" — agent should add --dependencies
349
377
 
@@ -358,7 +386,7 @@ def compact_view(sm: SourceMap, *, no_tree: bool = False) -> dict[str, Any]:
358
386
  )
359
387
  env_map_items = [
360
388
  {k: v for k, v in asdict(e).items() if v is not None and v != "" and v != []}
361
- for e in _sorted_env[:15]
389
+ for e in _sorted_env[:_ENV_MAP_CAP]
362
390
  ]
363
391
 
364
392
  code_notes_summary_dict: Any = None
@@ -373,13 +401,13 @@ def compact_view(sm: SourceMap, *, no_tree: bool = False) -> dict[str, Any]:
373
401
  )
374
402
  code_notes_items = [
375
403
  {k: v for k, v in asdict(n).items() if v is not None}
376
- for n in _sorted_notes[:20]
404
+ for n in _sorted_notes[:_CODE_NOTES_CAP]
377
405
  ]
378
406
 
379
- # Entry points: production runtime only. Auxiliary and development entries
380
- # are exposed separately so agents do not mix tooling with execution paths.
407
+ # Entry points: production runtime only, capped.
408
+ # Development entries shown separately; auxiliary omitted from compact view.
381
409
  ep_groups = _entry_point_groups(sm.entry_points)
382
- entry_points_compact = ep_groups["production"]
410
+ entry_points_compact = ep_groups["production"][:_EP_PRODUCTION_CAP]
383
411
  if not entry_points_compact:
384
412
  entry_points_compact = [] # truth signal: no production runtime detected
385
413
 
@@ -408,8 +436,7 @@ def compact_view(sm: SourceMap, *, no_tree: bool = False) -> dict[str, Any]:
408
436
  "context_summary": context_summary_dict,
409
437
  "stacks": [asdict(stack) for stack in sm.stacks],
410
438
  "entry_points": entry_points_compact,
411
- "development_entry_points": ep_groups["development"] or None,
412
- "auxiliary_entry_points": ep_groups["auxiliary"] or None,
439
+ "development_entry_points": (ep_groups["development"][:_EP_DEV_CAP] or None),
413
440
  "dependency_summary": dep_summary_dict,
414
441
  "key_dependencies": key_deps,
415
442
  "env_summary": env_summary_dict,
@@ -742,25 +769,23 @@ def agent_view(sm: SourceMap) -> dict[str, Any]:
742
769
 
743
770
  result: dict[str, Any] = {"project": project}
744
771
 
745
- # ── 2. Entry points: production/runtime only in the primary field ─────────
746
- # Development and auxiliary entries are explicit side channels. A missing
747
- # production runtime is represented as entry_points=[], never by fallback.
772
+ # ── 2. Entry points: production/runtime only, capped ─────────────────────
773
+ # Auxiliary entries are not actionable for agents — omitted entirely.
774
+ # Development entries shown in a separate channel, capped.
748
775
  ep_groups = _entry_point_groups(sm.entry_points)
749
- result["entry_points"] = ep_groups["production"]
776
+ result["entry_points"] = ep_groups["production"][:_EP_PRODUCTION_CAP]
750
777
  if ep_groups["development"]:
751
- result["development_entry_points"] = ep_groups["development"]
752
- if ep_groups["auxiliary"]:
753
- result["auxiliary_entry_points"] = ep_groups["auxiliary"]
778
+ result["development_entry_points"] = ep_groups["development"][:_EP_DEV_CAP]
754
779
 
755
780
  # ── 3. Architecture ───────────────────────────────────────────────────────
756
781
  result["architecture"] = _architecture_context(sm)
757
782
 
758
- # ── 3a. File relevance: evidence-backed categories, not keyword matches ──
783
+ # ── 3a. File relevance: evidence-backed, high-signal only ────────────────
759
784
  relevant_files = _file_relevance(sm)
760
785
  if relevant_files:
761
786
  result["file_relevance"] = relevant_files
762
787
 
763
- # ── 3b. Monorepo package roles (when available) ───────────────────────────
788
+ # ── 3b. Monorepo package roles (when available), capped ──────────────────
764
789
  if sm.monorepo_packages:
765
790
  _noise_roles = {"benchmark_layer", "tooling_layer", "docs_layer", "test_layer"}
766
791
  operational_pkgs = [
@@ -769,15 +794,16 @@ def agent_view(sm: SourceMap) -> dict[str, Any]:
769
794
  if p.architectural_role not in _noise_roles
770
795
  ]
771
796
  if operational_pkgs:
772
- result["runtime_packages"] = operational_pkgs
797
+ result["runtime_packages"] = operational_pkgs[:_MONOREPO_PKGS_CAP]
773
798
 
774
- # ── 4. Dependencies: separated by operational role ───────────────────────
799
+ # ── 4. Dependencies: separated by operational role, capped ───────────────
800
+ # noise_dependencies intentionally excluded — not actionable.
775
801
  dep_groups = _dependency_groups(sm)
776
802
  if dep_groups["production_dependencies"]:
777
- result["production_dependencies"] = dep_groups["production_dependencies"][:15]
778
- for dep_key in ("dev_tools", "test_utilities", "build_tooling", "noise_dependencies", "suspicious_dependencies"):
803
+ result["production_dependencies"] = dep_groups["production_dependencies"][:_PROD_DEPS_CAP]
804
+ for dep_key in ("dev_tools", "test_utilities", "build_tooling", "suspicious_dependencies"):
779
805
  if dep_groups[dep_key]:
780
- result[dep_key] = dep_groups[dep_key][:15]
806
+ result[dep_key] = dep_groups[dep_key][:_SECONDARY_DEPS_CAP]
781
807
 
782
808
  # Backward-compatible compact list, now production-only.
783
809
  production_key_deps = [
@@ -788,7 +814,7 @@ def agent_view(sm: SourceMap) -> dict[str, Any]:
788
814
  _dep_skip = {"parent", "manifest_path", "workspace", "source", "ecosystem"}
789
815
  result["key_dependencies"] = [
790
816
  {k: v for k, v in asdict(d).items() if v is not None and k not in _dep_skip}
791
- for d in production_key_deps[:15]
817
+ for d in production_key_deps[:_KEY_DEPS_CAP]
792
818
  ]
793
819
 
794
820
  # ── 5. Signals — compact operational context ─────────────────────────────
@@ -961,12 +987,12 @@ def standard_view(sm: SourceMap, *, include_tree: bool = False) -> dict[str, Any
961
987
  "project_summary": sm.project_summary,
962
988
  "architecture_summary": sm.architecture_summary,
963
989
  "stacks": [asdict(s) for s in sm.stacks],
964
- "entry_points": ep_groups["production"],
990
+ "entry_points": ep_groups["production"][:_EP_PRODUCTION_CAP],
965
991
  }
966
992
  if ep_groups["development"]:
967
- result["development_entry_points"] = ep_groups["development"]
993
+ result["development_entry_points"] = ep_groups["development"][:_EP_DEV_CAP]
968
994
  if ep_groups["auxiliary"]:
969
- result["auxiliary_entry_points"] = ep_groups["auxiliary"]
995
+ result["auxiliary_entry_points"] = ep_groups["auxiliary"][:_EP_DEV_CAP]
970
996
 
971
997
  # Layer B — signals (only when the corresponding analyzer ran)
972
998
  if sm.dependency_summary is not None and sm.dependency_summary.requested:
@@ -976,16 +1002,21 @@ def standard_view(sm: SourceMap, *, include_tree: bool = False) -> dict[str, Any
976
1002
  result["key_dependencies"] = [
977
1003
  asdict(d) for d in sm.key_dependencies
978
1004
  if (d.role or "unknown") in _PRODUCTION_DEP_ROLES and d.scope not in {"dev"}
979
- ]
1005
+ ][:_KEY_DEPS_CAP]
980
1006
 
981
1007
  if sm.env_summary is not None and sm.env_summary.requested:
982
1008
  result["env_summary"] = asdict(sm.env_summary)
983
- result["env_map"] = [asdict(e) for e in sm.env_map]
1009
+ result["env_map"] = [asdict(e) for e in sm.env_map[:_ENV_MAP_CAP]]
984
1010
 
985
1011
  if sm.code_notes_summary is not None and sm.code_notes_summary.requested:
986
1012
  result["code_notes_summary"] = asdict(sm.code_notes_summary)
987
1013
  if sm.code_notes:
988
- result["code_notes"] = [asdict(n) for n in sm.code_notes]
1014
+ _SEVERITY_ORDER = {"BUG": 0, "FIXME": 1, "DEPRECATED": 2, "TODO": 3, "HACK": 4, "WARNING": 5}
1015
+ _sorted_notes = sorted(
1016
+ sm.code_notes,
1017
+ key=lambda n: (_SEVERITY_ORDER.get(getattr(n, "kind", "").upper(), 9), getattr(n, "path", "")),
1018
+ )
1019
+ result["code_notes"] = [asdict(n) for n in _sorted_notes[:_CODE_NOTES_CAP]]
989
1020
  if sm.code_adrs:
990
1021
  result["code_adrs"] = [asdict(a) for a in sm.code_adrs]
991
1022
 
@@ -1076,12 +1107,12 @@ def _contract_view_minimal(
1076
1107
  """Minimal contract: project header + stripped per-file contracts."""
1077
1108
  primary = next((s for s in sm.stacks if s.primary), sm.stacks[0] if sm.stacks else None)
1078
1109
 
1079
- # Entry point paths only (production)
1110
+ # Entry point paths only (production), capped
1080
1111
  ep_paths = sorted({
1081
1112
  ep.path.replace("\\", "/")
1082
1113
  for ep in sm.entry_points
1083
1114
  if is_production_entry_point(ep)
1084
- })
1115
+ })[:_EP_PRODUCTION_CAP]
1085
1116
 
1086
1117
  project: dict[str, Any] = {"type": sm.project_type}
1087
1118
  if primary:
@@ -1174,6 +1205,33 @@ def _contract_view_minimal(
1174
1205
  if cs.symbol_truncation:
1175
1206
  result["symbol_query"] = cs.symbol_truncation
1176
1207
 
1208
+ # Monorepo package roles — helps agents understand workspace structure
1209
+ if sm.monorepo_packages:
1210
+ _noise_roles = {"benchmark_layer", "tooling_layer", "docs_layer", "test_layer"}
1211
+ operational_pkgs = [
1212
+ {"path": p.path, "role": p.architectural_role, "criticality": p.criticality}
1213
+ for p in sm.monorepo_packages
1214
+ if p.architectural_role not in _noise_roles
1215
+ ]
1216
+ if operational_pkgs:
1217
+ result["workspace_packages"] = operational_pkgs[:_MONOREPO_PKGS_CAP]
1218
+
1219
+ # Confidence summary — detection quality signal
1220
+ if sm.confidence_summary is not None:
1221
+ cs_conf = sm.confidence_summary
1222
+ conf: dict[str, Any] = {
1223
+ "overall": cs_conf.overall,
1224
+ "stack": cs_conf.stack_confidence,
1225
+ "entry_points": cs_conf.entry_point_confidence,
1226
+ }
1227
+ if cs_conf.anomalies:
1228
+ conf["anomalies"] = cs_conf.anomalies
1229
+ result["confidence"] = conf
1230
+
1231
+ # Analysis gaps — explicit about what could not be analyzed
1232
+ if sm.analysis_gaps:
1233
+ result["analysis_gaps"] = [asdict(g) for g in sm.analysis_gaps]
1234
+
1177
1235
  return result
1178
1236
 
1179
1237
 
@@ -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