sourcecode 0.45.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.45.0 → sourcecode-0.46.0}/PKG-INFO +1 -1
  2. {sourcecode-0.45.0 → sourcecode-0.46.0}/pyproject.toml +1 -1
  3. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/__init__.py +1 -1
  4. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/cli.py +18 -4
  5. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/prepare_context.py +38 -3
  6. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/serializer.py +27 -0
  7. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/workspace.py +35 -1
  8. {sourcecode-0.45.0 → sourcecode-0.46.0}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
  9. {sourcecode-0.45.0 → sourcecode-0.46.0}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
  10. {sourcecode-0.45.0 → sourcecode-0.46.0}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
  11. {sourcecode-0.45.0 → sourcecode-0.46.0}/.gitignore +0 -0
  12. {sourcecode-0.45.0 → sourcecode-0.46.0}/.ruff.toml +0 -0
  13. {sourcecode-0.45.0 → sourcecode-0.46.0}/CONTRIBUTING.md +0 -0
  14. {sourcecode-0.45.0 → sourcecode-0.46.0}/LICENSE +0 -0
  15. {sourcecode-0.45.0 → sourcecode-0.46.0}/README.md +0 -0
  16. {sourcecode-0.45.0 → sourcecode-0.46.0}/SECURITY.md +0 -0
  17. {sourcecode-0.45.0 → sourcecode-0.46.0}/docs/privacy.md +0 -0
  18. {sourcecode-0.45.0 → sourcecode-0.46.0}/docs/schema.md +0 -0
  19. {sourcecode-0.45.0 → sourcecode-0.46.0}/raw +0 -0
  20. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/adaptive_scanner.py +0 -0
  21. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/architecture_analyzer.py +0 -0
  22. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/architecture_summary.py +0 -0
  23. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/ast_extractor.py +0 -0
  24. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/classifier.py +0 -0
  25. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  26. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/confidence_analyzer.py +0 -0
  27. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/context_scorer.py +0 -0
  28. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/context_summarizer.py +0 -0
  29. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/contract_model.py +0 -0
  30. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/contract_pipeline.py +0 -0
  31. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/coverage_parser.py +0 -0
  32. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/dependency_analyzer.py +0 -0
  33. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/__init__.py +0 -0
  34. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/base.py +0 -0
  35. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  36. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/dart.py +0 -0
  37. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/dotnet.py +0 -0
  38. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/elixir.py +0 -0
  39. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/go.py +0 -0
  40. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/heuristic.py +0 -0
  41. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/hybrid.py +0 -0
  42. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/java.py +0 -0
  43. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  44. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/nodejs.py +0 -0
  45. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/parsers.py +0 -0
  46. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/php.py +0 -0
  47. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/project.py +0 -0
  48. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/python.py +0 -0
  49. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/ruby.py +0 -0
  50. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/rust.py +0 -0
  51. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/systems.py +0 -0
  52. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/terraform.py +0 -0
  53. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/detectors/tooling.py +0 -0
  54. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/doc_analyzer.py +0 -0
  55. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  56. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/env_analyzer.py +0 -0
  57. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/file_classifier.py +0 -0
  58. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/git_analyzer.py +0 -0
  59. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/graph_analyzer.py +0 -0
  60. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/metrics_analyzer.py +0 -0
  61. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/ranking_engine.py +0 -0
  62. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/redactor.py +0 -0
  63. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/relevance_scorer.py +0 -0
  64. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/repo_classifier.py +0 -0
  65. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/runtime_classifier.py +0 -0
  66. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/scanner.py +0 -0
  67. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/schema.py +0 -0
  68. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/semantic_analyzer.py +0 -0
  69. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/summarizer.py +0 -0
  70. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/telemetry/__init__.py +0 -0
  71. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/telemetry/config.py +0 -0
  72. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/telemetry/consent.py +0 -0
  73. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/telemetry/events.py +0 -0
  74. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/telemetry/filters.py +0 -0
  75. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/telemetry/transport.py +0 -0
  76. {sourcecode-0.45.0 → sourcecode-0.46.0}/src/sourcecode/tree_utils.py +0 -0
  77. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/__init__.py +0 -0
  78. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/conftest.py +0 -0
  79. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/fixtures/coverage.xml +0 -0
  80. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  81. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  82. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  83. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/fixtures/go_service/go.mod +0 -0
  84. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/fixtures/jacoco.xml +0 -0
  85. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/fixtures/lcov.info +0 -0
  86. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  87. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/fixtures/nextjs_app/package.json +0 -0
  88. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  89. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  90. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  91. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  92. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  93. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  94. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_architecture_analyzer.py +0 -0
  95. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_architecture_summary.py +0 -0
  96. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_ast_extractor.py +0 -0
  97. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_block1_reliability.py +0 -0
  98. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_block2_coverage.py +0 -0
  99. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_block5_quality.py +0 -0
  100. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_classifier.py +0 -0
  101. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_cli.py +0 -0
  102. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_code_notes_analyzer.py +0 -0
  103. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_context_scorer.py +0 -0
  104. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_contract_pipeline.py +0 -0
  105. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_coverage_parser.py +0 -0
  106. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_cross_consistency.py +0 -0
  107. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_dependency_analyzer_node_python.py +0 -0
  108. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  109. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_dependency_schema.py +0 -0
  110. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_detector_dotnet.py +0 -0
  111. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_detector_go_rust_java.py +0 -0
  112. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_detector_nodejs.py +0 -0
  113. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_detector_php_ruby_dart.py +0 -0
  114. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_detector_python.py +0 -0
  115. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_detector_universal_managed.py +0 -0
  116. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_detector_universal_systems.py +0 -0
  117. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_detectors_base.py +0 -0
  118. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  119. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_doc_analyzer_python.py +0 -0
  120. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  121. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_graph_analyzer_python_node.py +0 -0
  122. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_graph_schema.py +0 -0
  123. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_hybrid_inference.py +0 -0
  124. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_integration.py +0 -0
  125. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_integration_dependencies.py +0 -0
  126. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_integration_detection.py +0 -0
  127. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_integration_docs.py +0 -0
  128. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_integration_graph_modules.py +0 -0
  129. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_integration_lqn.py +0 -0
  130. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_integration_metrics.py +0 -0
  131. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_integration_multistack.py +0 -0
  132. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_integration_semantics.py +0 -0
  133. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_integration_universal.py +0 -0
  134. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_metrics_analyzer.py +0 -0
  135. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_packaging.py +0 -0
  136. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_phase1_improvements.py +0 -0
  137. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_pipeline_integrity.py +0 -0
  138. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_real_projects.py +0 -0
  139. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_redactor.py +0 -0
  140. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_scanner.py +0 -0
  141. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_schema.py +0 -0
  142. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_schema_normalization.py +0 -0
  143. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_semantic_analyzer_node.py +0 -0
  144. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_semantic_analyzer_python.py +0 -0
  145. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_semantic_import_resolution.py +0 -0
  146. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_semantic_schema.py +0 -0
  147. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_signal_hierarchy.py +0 -0
  148. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_summarizer.py +0 -0
  149. {sourcecode-0.45.0 → sourcecode-0.46.0}/tests/test_telemetry.py +0 -0
  150. {sourcecode-0.45.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.45.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.45.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.45.0"
3
+ __version__ = "0.46.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,
@@ -1174,6 +1174,33 @@ def _contract_view_minimal(
1174
1174
  if cs.symbol_truncation:
1175
1175
  result["symbol_query"] = cs.symbol_truncation
1176
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
+
1177
1204
  return result
1178
1205
 
1179
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