sourcecode 0.41.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.
- {sourcecode-0.41.0 → sourcecode-0.43.0}/PKG-INFO +1 -1
- {sourcecode-0.41.0 → sourcecode-0.43.0}/pyproject.toml +1 -1
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/architecture_analyzer.py +94 -8
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/cli.py +28 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/contract_model.py +1 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/contract_pipeline.py +49 -14
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/doc_analyzer.py +22 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/env_analyzer.py +110 -22
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/git_analyzer.py +13 -2
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/prepare_context.py +6 -2
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/schema.py +29 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/semantic_analyzer.py +64 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/serializer.py +44 -7
- sourcecode-0.43.0/tests/test_block1_reliability.py +474 -0
- sourcecode-0.43.0/tests/test_block2_coverage.py +449 -0
- sourcecode-0.43.0/tests/test_block5_quality.py +302 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/.gitignore +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/.ruff.toml +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/CONTRIBUTING.md +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/LICENSE +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/README.md +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/SECURITY.md +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/docs/privacy.md +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/docs/schema.md +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/raw +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/__init__.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/conftest.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/coverage.xml +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/go_service/go.mod +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/jacoco.xml +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/lcov.info +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/nextjs_app/package.json +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_architecture_analyzer.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_architecture_summary.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_ast_extractor.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_classifier.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_cli.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_code_notes_analyzer.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_contract_pipeline.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_coverage_parser.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_cross_consistency.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_dependency_analyzer_node_python.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_dependency_schema.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detector_dotnet.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detector_go_rust_java.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detector_nodejs.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detector_php_ruby_dart.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detector_python.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detector_universal_managed.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detector_universal_systems.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detectors_base.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_doc_analyzer_jsdom.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_doc_analyzer_python.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_graph_analyzer_polyglot.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_graph_analyzer_python_node.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_graph_schema.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_hybrid_inference.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_dependencies.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_detection.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_docs.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_graph_modules.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_lqn.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_metrics.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_multistack.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_semantics.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_universal.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_metrics_analyzer.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_packaging.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_phase1_improvements.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_pipeline_integrity.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_real_projects.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_redactor.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_scanner.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_schema.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_schema_normalization.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_semantic_analyzer_node.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_semantic_analyzer_python.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_semantic_import_resolution.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_semantic_schema.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_signal_hierarchy.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_summarizer.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_telemetry.py +0 -0
- {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_workspace_analyzer.py +0 -0
|
@@ -172,6 +172,7 @@ class ArchitectureAnalyzer:
|
|
|
172
172
|
graph: Optional[ModuleGraph] = None,
|
|
173
173
|
) -> ArchitectureAnalysis:
|
|
174
174
|
limitations: list[str] = []
|
|
175
|
+
evidence: list[dict] = []
|
|
175
176
|
|
|
176
177
|
# Step 1: filter paths
|
|
177
178
|
filtered = self._filter_paths(sm.file_paths)
|
|
@@ -180,6 +181,8 @@ class ArchitectureAnalyzer:
|
|
|
180
181
|
requested=True,
|
|
181
182
|
pattern="unknown",
|
|
182
183
|
limitations=["Arquitectura no inferida: proyecto sin archivos de codigo suficientes"],
|
|
184
|
+
evidence=[{"type": "none", "paths": [], "reason": "insufficient source files", "confidence": "high"}],
|
|
185
|
+
tentative=False,
|
|
183
186
|
)
|
|
184
187
|
|
|
185
188
|
# Step 2: domain clustering
|
|
@@ -193,17 +196,32 @@ class ArchitectureAnalyzer:
|
|
|
193
196
|
elif pattern == "unknown":
|
|
194
197
|
limitations.append("Patron de capas no reconocido: estructura de directorios sin senales claras")
|
|
195
198
|
|
|
196
|
-
# Step 3b: monorepo override — workspace config is hard evidence
|
|
197
|
-
|
|
199
|
+
# Step 3b: monorepo override — workspace config is hard evidence.
|
|
200
|
+
# Overrides all weak inferred patterns; only truly specialised patterns
|
|
201
|
+
# (cqrs, clean, onion, hexagonal) take precedence over workspace config.
|
|
202
|
+
has_workspace = self._has_workspace_config(sm.file_paths)
|
|
203
|
+
if has_workspace and pattern not in (
|
|
198
204
|
"monorepo", "cqrs", "clean", "onion", "hexagonal"
|
|
199
205
|
):
|
|
200
206
|
mono_layers = self._detect_monorepo_packages(filtered)
|
|
201
|
-
|
|
207
|
+
# Override whenever: monorepo packages detected, OR pattern is any weak/generic type.
|
|
208
|
+
# "fullstack", "layered", "mvc", "microservices", "modular", "flat", "unknown", None
|
|
209
|
+
# all yield to workspace config evidence.
|
|
210
|
+
_WEAK_PATTERNS = {None, "unknown", "flat", "modular", "layered",
|
|
211
|
+
"fullstack", "mvc", "microservices"}
|
|
212
|
+
if mono_layers or pattern in _WEAK_PATTERNS:
|
|
202
213
|
pattern = "monorepo"
|
|
203
214
|
layers = mono_layers
|
|
204
215
|
limitations.append(
|
|
205
216
|
"Workspace config detectado — arquitectura refleja topologia de paquetes"
|
|
206
217
|
)
|
|
218
|
+
ws_files = [p for p in sm.file_paths if p.split("/")[-1] in _WORKSPACE_CONFIG_FILES]
|
|
219
|
+
evidence.append({
|
|
220
|
+
"type": "workspace_config",
|
|
221
|
+
"paths": ws_files[:4],
|
|
222
|
+
"reason": "Monorepo workspace config file(s) detected — hard evidence for monorepo topology",
|
|
223
|
+
"confidence": "high",
|
|
224
|
+
})
|
|
207
225
|
|
|
208
226
|
# Step 4: bounded context inference
|
|
209
227
|
bounded_contexts = self._infer_bounded_contexts(domains, graph)
|
|
@@ -212,25 +230,91 @@ class ArchitectureAnalyzer:
|
|
|
212
230
|
confidence: Literal["high", "medium", "low"]
|
|
213
231
|
strong_domains = [d for d in domains if d.confidence in ("high", "medium")]
|
|
214
232
|
all_layers_weak = layers and all(l.confidence == "low" for l in layers)
|
|
233
|
+
|
|
234
|
+
method = "graph+structure" if graph is not None else "filesystem_inference"
|
|
235
|
+
# High-confidence evidence (workspace config) makes pattern non-tentative.
|
|
236
|
+
tentative = not any(e.get("confidence") == "high" for e in evidence)
|
|
237
|
+
|
|
238
|
+
# _hard_evidence: high-confidence evidence was already set (e.g. workspace_config).
|
|
239
|
+
# When True, tentative must stay False and confidence must stay at least "medium".
|
|
240
|
+
_hard_evidence = not tentative # tentative=False iff high-conf evidence present
|
|
241
|
+
|
|
215
242
|
if pattern not in (None, "unknown", "flat"):
|
|
216
|
-
if
|
|
243
|
+
if graph is not None:
|
|
244
|
+
# Import graph provided — structural validation available
|
|
245
|
+
confidence = "medium" if len(strong_domains) >= 3 else "low"
|
|
246
|
+
evidence.append({
|
|
247
|
+
"type": "import_graph",
|
|
248
|
+
"paths": [n.id for n in graph.nodes[:6]],
|
|
249
|
+
"reason": f"Module import graph with {len(graph.nodes)} nodes used for pattern validation",
|
|
250
|
+
"confidence": "medium",
|
|
251
|
+
})
|
|
252
|
+
elif all_layers_weak:
|
|
217
253
|
# Layers came from file-naming heuristic only, not directory structure
|
|
218
254
|
confidence = "low"
|
|
255
|
+
if not _hard_evidence:
|
|
256
|
+
tentative = True
|
|
219
257
|
limitations.append(
|
|
220
258
|
"Low confidence inference: pattern inferred from filenames only, without import graph confirmation"
|
|
221
259
|
)
|
|
260
|
+
evidence.append({
|
|
261
|
+
"type": "filesystem_naming",
|
|
262
|
+
"paths": [l.files[0] for l in layers if l.files][:6],
|
|
263
|
+
"reason": (
|
|
264
|
+
f"Pattern '{pattern}' inferred from file stem naming conventions only "
|
|
265
|
+
"(e.g. *_controller.py, *_service.py). "
|
|
266
|
+
"No directory structure or import graph confirmation."
|
|
267
|
+
),
|
|
268
|
+
"confidence": "low",
|
|
269
|
+
})
|
|
222
270
|
else:
|
|
223
|
-
|
|
224
|
-
if
|
|
271
|
+
# Directory structure match (or monorepo/workspace override with no layers)
|
|
272
|
+
confidence = "medium" if (_hard_evidence or len(strong_domains) >= 3) else "low"
|
|
273
|
+
if confidence == "low" and not _hard_evidence:
|
|
274
|
+
tentative = True
|
|
275
|
+
if not _hard_evidence:
|
|
225
276
|
limitations.append(
|
|
226
277
|
"Pattern not confirmed by module import graph; run with --graph-modules for structural validation"
|
|
227
278
|
)
|
|
279
|
+
if not _hard_evidence:
|
|
280
|
+
matched_dirs = sorted({
|
|
281
|
+
p.replace("\\", "/").split("/")[0]
|
|
282
|
+
for layer in layers for p in layer.files
|
|
283
|
+
})
|
|
284
|
+
evidence.append({
|
|
285
|
+
"type": "filesystem_naming",
|
|
286
|
+
"paths": matched_dirs[:8],
|
|
287
|
+
"reason": (
|
|
288
|
+
f"Pattern '{pattern}' inferred from directory names matching layer keywords. "
|
|
289
|
+
"Import graph not available — structural direction of dependencies unverified."
|
|
290
|
+
),
|
|
291
|
+
"confidence": "low" if confidence == "low" else "medium",
|
|
292
|
+
})
|
|
228
293
|
elif len(strong_domains) >= 1:
|
|
229
294
|
confidence = "medium"
|
|
295
|
+
if not _hard_evidence:
|
|
296
|
+
tentative = True
|
|
297
|
+
evidence.append({
|
|
298
|
+
"type": "filesystem_naming",
|
|
299
|
+
"paths": [d.name for d in strong_domains[:6]],
|
|
300
|
+
"reason": "Domain clustering from directory names; no layer pattern confirmed",
|
|
301
|
+
"confidence": "low",
|
|
302
|
+
})
|
|
230
303
|
else:
|
|
231
304
|
confidence = "low"
|
|
232
|
-
|
|
233
|
-
|
|
305
|
+
if not _hard_evidence:
|
|
306
|
+
tentative = True
|
|
307
|
+
if not evidence:
|
|
308
|
+
limitations.append(
|
|
309
|
+
"insufficient_evidence: no recognizable architectural signals found; "
|
|
310
|
+
"filesystem structure does not match known patterns"
|
|
311
|
+
)
|
|
312
|
+
evidence.append({
|
|
313
|
+
"type": "filesystem_naming",
|
|
314
|
+
"paths": filtered[:6],
|
|
315
|
+
"reason": "Only filesystem paths available; no pattern matched",
|
|
316
|
+
"confidence": "low",
|
|
317
|
+
})
|
|
234
318
|
|
|
235
319
|
return ArchitectureAnalysis(
|
|
236
320
|
requested=True,
|
|
@@ -241,6 +325,8 @@ class ArchitectureAnalyzer:
|
|
|
241
325
|
confidence=confidence,
|
|
242
326
|
method=method,
|
|
243
327
|
limitations=limitations,
|
|
328
|
+
evidence=evidence,
|
|
329
|
+
tentative=tentative,
|
|
244
330
|
)
|
|
245
331
|
|
|
246
332
|
# ------------------------------------------------------------------
|
|
@@ -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:
|
|
@@ -45,9 +45,10 @@ def _get_changed_files(root: Path) -> set[str]:
|
|
|
45
45
|
]:
|
|
46
46
|
try:
|
|
47
47
|
result = subprocess.run(
|
|
48
|
-
cmd, cwd=root, capture_output=True, text=True,
|
|
48
|
+
cmd, cwd=root, capture_output=True, text=True,
|
|
49
|
+
encoding="utf-8", errors="replace", timeout=10,
|
|
49
50
|
)
|
|
50
|
-
for line in result.stdout.splitlines():
|
|
51
|
+
for line in (result.stdout or "").splitlines():
|
|
51
52
|
line = line.strip()
|
|
52
53
|
if line:
|
|
53
54
|
changed.add(line.replace("\\", "/"))
|
|
@@ -56,9 +57,10 @@ def _get_changed_files(root: Path) -> set[str]:
|
|
|
56
57
|
try:
|
|
57
58
|
result = subprocess.run(
|
|
58
59
|
["git", "status", "--porcelain"],
|
|
59
|
-
cwd=root, capture_output=True, text=True,
|
|
60
|
+
cwd=root, capture_output=True, text=True,
|
|
61
|
+
encoding="utf-8", errors="replace", timeout=10,
|
|
60
62
|
)
|
|
61
|
-
for line in result.stdout.splitlines():
|
|
63
|
+
for line in (result.stdout or "").splitlines():
|
|
62
64
|
if len(line) > 3:
|
|
63
65
|
changed.add(line[3:].strip().replace("\\", "/"))
|
|
64
66
|
except Exception:
|
|
@@ -129,11 +131,12 @@ def _get_git_churn(root: Path, file_paths: list[str]) -> dict[str, int]:
|
|
|
129
131
|
try:
|
|
130
132
|
result = subprocess.run(
|
|
131
133
|
["git", "log", "--name-only", "--format=", "--since=90.days.ago"],
|
|
132
|
-
cwd=root, capture_output=True, text=True,
|
|
134
|
+
cwd=root, capture_output=True, text=True,
|
|
135
|
+
encoding="utf-8", errors="replace", timeout=15,
|
|
133
136
|
)
|
|
134
137
|
path_set = set(file_paths)
|
|
135
138
|
counter: Counter[str] = Counter()
|
|
136
|
-
for line in result.stdout.splitlines():
|
|
139
|
+
for line in (result.stdout or "").splitlines():
|
|
137
140
|
line = line.strip().replace("\\", "/")
|
|
138
141
|
if line in path_set:
|
|
139
142
|
counter[line] += 1
|
|
@@ -172,6 +175,7 @@ class ContractPipeline:
|
|
|
172
175
|
changed_only: bool = False,
|
|
173
176
|
symbol: Optional[str] = None,
|
|
174
177
|
compress_types: bool = False,
|
|
178
|
+
max_importers: int = 50,
|
|
175
179
|
) -> tuple[list[FileContract], ContractSummary]:
|
|
176
180
|
"""Run the full extraction pipeline.
|
|
177
181
|
|
|
@@ -276,17 +280,19 @@ class ContractPipeline:
|
|
|
276
280
|
contracts = self._rank(contracts, rank_by)
|
|
277
281
|
|
|
278
282
|
# 8. Symbol filter — keep files that define or import the symbol
|
|
283
|
+
_symbol_truncation: Optional[dict] = None
|
|
279
284
|
if symbol:
|
|
280
|
-
contracts = _filter_by_symbol(contracts, symbol)
|
|
285
|
+
contracts, _symbol_truncation = _filter_by_symbol(contracts, symbol, max_importers=max_importers)
|
|
281
286
|
# When shallow scan missed the defining file (deep monorepo), fall back
|
|
282
287
|
# to a grep-based filesystem search over the full directory tree.
|
|
283
288
|
if not contracts:
|
|
284
|
-
contracts = self._symbol_deep_scan(
|
|
289
|
+
contracts, _symbol_truncation = self._symbol_deep_scan(
|
|
285
290
|
root, symbol,
|
|
286
291
|
known_paths=set(src_paths),
|
|
287
292
|
entry_paths=entry_paths,
|
|
288
293
|
changed_files=changed_files,
|
|
289
294
|
engine=engine,
|
|
295
|
+
max_importers=max_importers,
|
|
290
296
|
)
|
|
291
297
|
|
|
292
298
|
# 9. Entrypoints-only filter
|
|
@@ -310,6 +316,7 @@ class ContractPipeline:
|
|
|
310
316
|
method_breakdown=dict(method_counts),
|
|
311
317
|
ranked_by=rank_by,
|
|
312
318
|
limitations=limitations,
|
|
319
|
+
symbol_truncation=_symbol_truncation,
|
|
313
320
|
)
|
|
314
321
|
return contracts, summary
|
|
315
322
|
|
|
@@ -329,7 +336,8 @@ class ContractPipeline:
|
|
|
329
336
|
entry_paths: set[str],
|
|
330
337
|
changed_files: set[str],
|
|
331
338
|
engine: RankingEngine,
|
|
332
|
-
|
|
339
|
+
max_importers: int = 50,
|
|
340
|
+
) -> tuple[list[FileContract], dict]:
|
|
333
341
|
"""Grep-based fallback when the shallow scan missed the defining files.
|
|
334
342
|
|
|
335
343
|
Searches the full directory tree for source files containing *symbol*,
|
|
@@ -353,7 +361,7 @@ class ContractPipeline:
|
|
|
353
361
|
contract.ranking_reasons = fs.reasons
|
|
354
362
|
extra.append(contract)
|
|
355
363
|
|
|
356
|
-
return _filter_by_symbol(extra, symbol)
|
|
364
|
+
return _filter_by_symbol(extra, symbol, max_importers=max_importers)
|
|
357
365
|
|
|
358
366
|
|
|
359
367
|
# ---------------------------------------------------------------------------
|
|
@@ -409,7 +417,11 @@ def _limit_symbols(contracts: list[FileContract], max_symbols: int) -> list[File
|
|
|
409
417
|
# Symbol-aware filter
|
|
410
418
|
# ---------------------------------------------------------------------------
|
|
411
419
|
|
|
412
|
-
def _filter_by_symbol(
|
|
420
|
+
def _filter_by_symbol(
|
|
421
|
+
contracts: list[FileContract],
|
|
422
|
+
symbol: str,
|
|
423
|
+
max_importers: int = 50,
|
|
424
|
+
) -> tuple[list[FileContract], dict]:
|
|
413
425
|
"""Return contracts that define, import, or structurally reference *symbol*.
|
|
414
426
|
|
|
415
427
|
Four tiers applied in order:
|
|
@@ -420,6 +432,8 @@ def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileCo
|
|
|
420
432
|
function signatures (word-boundary). Only used when tiers 1-3 fail.
|
|
421
433
|
|
|
422
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).
|
|
423
437
|
"""
|
|
424
438
|
sym_l = symbol.lower()
|
|
425
439
|
word_re = re.compile(
|
|
@@ -463,8 +477,14 @@ def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileCo
|
|
|
463
477
|
|
|
464
478
|
# Tier 3: import matching (case-insensitive when no definers found)
|
|
465
479
|
ci_imports = len(defining) == 0
|
|
466
|
-
|
|
467
|
-
|
|
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}
|
|
468
488
|
|
|
469
489
|
# Tier 4: type-reference matching (only when tiers 1-3 yield nothing)
|
|
470
490
|
references: list[FileContract] = []
|
|
@@ -480,12 +500,27 @@ def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileCo
|
|
|
480
500
|
seen.add(c.path)
|
|
481
501
|
merged.append(c)
|
|
482
502
|
|
|
483
|
-
|
|
503
|
+
result = sorted(merged, key=lambda c: (
|
|
484
504
|
c.path not in defining_paths,
|
|
485
505
|
c.path not in importer_paths,
|
|
486
506
|
-c.relevance_score,
|
|
487
507
|
))
|
|
488
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
|
+
|
|
489
524
|
|
|
490
525
|
# ---------------------------------------------------------------------------
|
|
491
526
|
# Deep symbol scan — grep-based fallback for shallow-scanned repos
|
|
@@ -132,6 +132,8 @@ class DocAnalyzer:
|
|
|
132
132
|
records: list[DocRecord] = []
|
|
133
133
|
limitations: list[str] = list(limitations_pre)
|
|
134
134
|
languages: set[str] = set()
|
|
135
|
+
# Track per-language support status for honest reporting
|
|
136
|
+
unsupported_langs: set[str] = set()
|
|
135
137
|
|
|
136
138
|
for relative_path in file_paths:
|
|
137
139
|
abs_path = root / relative_path
|
|
@@ -176,8 +178,18 @@ class DocAnalyzer:
|
|
|
176
178
|
# Unsupported language — D-04: no emitir DocRecord, solo registrar limitation
|
|
177
179
|
limitations.append(f"docs_unavailable:{norm_path}:language={lang}")
|
|
178
180
|
languages.add(lang)
|
|
181
|
+
unsupported_langs.add(lang)
|
|
179
182
|
# NO records.append() here
|
|
180
183
|
|
|
184
|
+
# Build language_coverage: explicit per-language support status
|
|
185
|
+
_SUPPORTED_LANGS = {"python", "javascript", "typescript"}
|
|
186
|
+
lang_coverage: dict[str, str] = {}
|
|
187
|
+
for lang in languages:
|
|
188
|
+
if lang in _SUPPORTED_LANGS:
|
|
189
|
+
lang_coverage[lang] = "supported"
|
|
190
|
+
else:
|
|
191
|
+
lang_coverage[lang] = "unsupported"
|
|
192
|
+
|
|
181
193
|
# Build summary
|
|
182
194
|
symbol_count = sum(1 for r in records if r.kind != "module")
|
|
183
195
|
total_count = len(records)
|
|
@@ -192,6 +204,15 @@ class DocAnalyzer:
|
|
|
192
204
|
"no docstrings or JSDoc comments found"
|
|
193
205
|
)
|
|
194
206
|
|
|
207
|
+
# Warn explicitly when unsupported languages are present — agents must not
|
|
208
|
+
# assume full coverage when Java/Go/Rust files are in scope but not analyzed.
|
|
209
|
+
if unsupported_langs:
|
|
210
|
+
sorted_unsupported = sorted(unsupported_langs)
|
|
211
|
+
limitations.append(
|
|
212
|
+
f"docs_not_extracted: language(s) {sorted_unsupported} present but not supported; "
|
|
213
|
+
"only Python and JS/TS docstrings are extracted"
|
|
214
|
+
)
|
|
215
|
+
|
|
195
216
|
summary = DocSummary(
|
|
196
217
|
requested=True,
|
|
197
218
|
total_count=total_count,
|
|
@@ -200,6 +221,7 @@ class DocAnalyzer:
|
|
|
200
221
|
depth=depth,
|
|
201
222
|
truncated=truncated,
|
|
202
223
|
limitations=limitations,
|
|
224
|
+
language_coverage=lang_coverage,
|
|
203
225
|
)
|
|
204
226
|
return records, summary
|
|
205
227
|
|