sourcecode 0.31.0__py3-none-any.whl → 0.32.0__py3-none-any.whl

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/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
- """sourcecode — Genera mapas de contexto estructurado para agentes IA."""
1
+ """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "0.31.0"
3
+ __version__ = "0.32.0"
sourcecode/cli.py CHANGED
@@ -6,10 +6,10 @@ import time
6
6
  from pathlib import Path
7
7
  from typing import Any, Optional, cast
8
8
 
9
- import typer
10
-
11
- from sourcecode import __version__
12
- from sourcecode.entrypoint_classifier import is_production_entry_point, normalize_entry_point
9
+ import typer
10
+
11
+ from sourcecode import __version__
12
+ from sourcecode.entrypoint_classifier import is_production_entry_point, normalize_entry_point
13
13
 
14
14
 
15
15
  # ---------------------------------------------------------------------------
@@ -118,11 +118,11 @@ def _check_pipeline_coherence(sm: "SourceMap") -> list[str]: # type: ignore[nam
118
118
  )
119
119
 
120
120
  # overall:high requires at least one production entry point
121
- if cs.overall == "high":
122
- prod_eps = [
123
- ep for ep in sm.entry_points
124
- if is_production_entry_point(ep)
125
- ]
121
+ if cs.overall == "high":
122
+ prod_eps = [
123
+ ep for ep in sm.entry_points
124
+ if is_production_entry_point(ep)
125
+ ]
126
126
  if not prod_eps and sm.entry_points:
127
127
  issues.append(
128
128
  "[coherence] overall=high but no production entry points exist — "
@@ -135,7 +135,7 @@ def _check_pipeline_coherence(sm: "SourceMap") -> list[str]: # type: ignore[nam
135
135
  "[coherence] entry_point_confidence=high but entry_points is empty"
136
136
  )
137
137
 
138
- return issues
138
+ return issues
139
139
 
140
140
  _HELP = """\
141
141
  Deterministic codebase context for AI coding agents.
@@ -323,61 +323,79 @@ def main(
323
323
  "json",
324
324
  "--format",
325
325
  "-f",
326
- help="Formato de salida: json|yaml",
326
+ help="Output format: json (default) or yaml. Both carry identical data — yaml is more human-readable, json is preferred for agent pipelines.",
327
327
  show_default=True,
328
328
  ),
329
329
  output: Optional[Path] = typer.Option(
330
330
  None,
331
331
  "--output",
332
332
  "-o",
333
- help="Fichero de salida (default: stdout)",
333
+ help="Write output to a file instead of stdout. Useful for storing analysis snapshots or piping to downstream tools.",
334
334
  ),
335
335
  compact: bool = typer.Option(
336
336
  False,
337
337
  "--compact",
338
- help="Output reducido (~500-700 tokens): tipo, stacks, entradas, arbol nivel 1 y summaries de flags opcionales activos",
338
+ help=(
339
+ "Compact output (~600–800 tokens): project type, stacks, production entry points, "
340
+ "dependency summary, confidence summary, and analysis gaps. "
341
+ "Omits file tree, raw dependency lists, docs, and module graph. "
342
+ "Designed for agent context windows. Automatically enables --dependencies, --env-map, and --code-notes."
343
+ ),
339
344
  ),
340
345
  dependencies: bool = typer.Option(
341
346
  False,
342
347
  "--dependencies",
343
- help="Incluir dependencias directas, versiones exactas y transitivas cuando haya lockfiles compatibles",
348
+ help=(
349
+ "Analyze direct and transitive dependencies. Reads manifests (pyproject.toml, package.json, go.mod, etc.) "
350
+ "and lockfiles when available. Adds dependency_summary and key_dependencies to output."
351
+ ),
344
352
  ),
345
353
  graph_modules: bool = typer.Option(
346
354
  False,
347
355
  "--graph-modules",
348
- help="Incluir grafo estructural de modulos, imports y relaciones simples del codigo",
356
+ help=(
357
+ "Include a structural module graph: nodes (files/symbols) and edges (imports, calls, contains). "
358
+ "Useful for understanding coupling and call flows. Adds module_graph to output. "
359
+ "Combine with --graph-detail and --graph-edges to control scope."
360
+ ),
349
361
  ),
350
362
  graph_detail: str = typer.Option(
351
363
  "high",
352
364
  "--graph-detail",
353
- help="Nivel de detalle del grafo: high|medium|full",
365
+ help="Detail level for --graph-modules: high (top modules by importance), medium (filtered by relevance), full (all nodes and edges). Default: high.",
354
366
  show_default=True,
355
367
  ),
356
368
  max_nodes: Optional[int] = typer.Option(
357
369
  None,
358
370
  "--max-nodes",
359
- help="Limite de nodos para `--graph-modules` en modos high/medium",
371
+ help="Maximum number of nodes in --graph-modules output when using high or medium detail. Prevents oversized graphs in large codebases.",
360
372
  min=1,
361
373
  ),
362
374
  graph_edges: Optional[str] = typer.Option(
363
375
  None,
364
376
  "--graph-edges",
365
- help="Tipos de arista para `--graph-modules` separados por comas: imports,calls,contains,extends",
377
+ help="Edge types for --graph-modules, comma-separated: imports,calls,contains,extends. Default: all available. Example: --graph-edges imports,calls",
366
378
  ),
367
379
  no_tree: bool = typer.Option(
368
380
  False,
369
381
  "--no-tree",
370
- help="Suprimir file_tree y file_paths del output (ahora deprecado: el arbol ya no se incluye por defecto)",
382
+ help="(Deprecated) Previously suppressed file_tree. The file tree is excluded by default this flag is now a no-op. Use --tree to include the file tree.",
371
383
  ),
372
384
  tree: bool = typer.Option(
373
385
  False,
374
386
  "--tree",
375
- help="Incluir file_tree completo y file_paths en el output (capa deep-dive)",
387
+ help=(
388
+ "Include the full file_tree and flat file_paths list in output (deep-dive layer). "
389
+ "Adds significant size — use when the agent needs to browse the full file structure."
390
+ ),
376
391
  ),
377
392
  no_redact: bool = typer.Option(
378
393
  False,
379
394
  "--no-redact",
380
- help="Desactivar redaccion de secretos (activa por defecto)",
395
+ help=(
396
+ "Disable automatic secret redaction. By default, potential secrets (API keys, tokens, passwords) "
397
+ "are replaced with [REDACTED]. Use with caution — output may contain sensitive values."
398
+ ),
381
399
  ),
382
400
  version: Optional[bool] = typer.Option(
383
401
  None,
@@ -385,80 +403,109 @@ def main(
385
403
  "-v",
386
404
  callback=version_callback,
387
405
  is_eager=True,
388
- help="Mostrar version y salir",
406
+ help="Show version number and exit.",
389
407
  ),
390
408
  depth: int = typer.Option(
391
409
  4,
392
410
  "--depth",
393
- help="Profundidad maxima del arbol de ficheros (default: 4)",
411
+ help=(
412
+ "Maximum depth for file tree traversal (default: 4, range: 1–20). "
413
+ "Increase for deeply nested projects — Maven/Java requires at least 8 (src/main/java/...)."
414
+ ),
394
415
  min=1,
395
416
  max=20,
396
417
  ),
397
418
  docs: bool = typer.Option(
398
419
  False,
399
420
  "--docs",
400
- help="Incluir documentacion extraida: docstrings, firmas y comentarios de modulos y simbolos",
421
+ help="Extract documentation: docstrings, function signatures, and module-level comments. Adds doc_summary and docs to output. Combine with --docs-depth to control coverage.",
401
422
  ),
402
423
  docs_depth: str = typer.Option(
403
424
  "symbols",
404
425
  "--docs-depth",
405
- help="Profundidad de extraccion de docs: module|symbols|full",
426
+ help="Documentation extraction depth: module (module-level only), symbols (functions and classes), full (all symbols including private). Default: symbols.",
406
427
  show_default=True,
407
428
  ),
408
429
  full_metrics: bool = typer.Option(
409
430
  False,
410
431
  "--full-metrics",
411
- help="Auditoria tecnica: LOC, simbolos, complejidad ciclomatica y cobertura por fichero. No incluido en --agent (uso: CI, code review, no context principal para agentes IA)",
432
+ help=(
433
+ "Technical audit: lines of code, symbol counts, cyclomatic complexity, and test coverage per file. "
434
+ "Produces file_metrics and metrics_summary. "
435
+ "Not included in --agent output — designed for CI pipelines and code review tools, not as primary agent context."
436
+ ),
412
437
  ),
413
438
  semantics: bool = typer.Option(
414
439
  False,
415
440
  "--semantics",
416
- help="Incluir call graph semantico, linking cross-file de simbolos y resolucion avanzada de imports",
441
+ help=(
442
+ "Semantic analysis: cross-file symbol resolution, call graph with confidence levels, and import linking. "
443
+ "Adds semantic_calls, semantic_symbols, semantic_links, semantic_summary, and hotspots (files ranked by fan-in/fan-out). "
444
+ "Slower than default analysis — skip for quick scans. "
445
+ "Confidence degrades on dynamic dispatch, decorators, and generated code."
446
+ ),
417
447
  ),
418
448
  architecture: bool = typer.Option(
419
449
  False,
420
450
  "--architecture",
421
- help="Inferencia arquitectonica: dominios funcionales, capas (MVC/layered/hexagonal) y bounded contexts aproximados",
451
+ help=(
452
+ "Architectural inference: detect functional layers (MVC/layered/hexagonal), bounded contexts, "
453
+ "and dominant structural patterns. Adds architecture to output. "
454
+ "Confidence is low when based on directory names alone — combine with --semantics for higher accuracy."
455
+ ),
422
456
  ),
423
457
  git_context: bool = typer.Option(
424
458
  False,
425
459
  "--git-context",
426
460
  "-g",
427
- help="Incluir contexto git: commits recientes, ficheros mas activos y cambios pendientes",
461
+ help="Include git activity: recent commits, change hotspots (most frequently modified files), pending uncommitted changes, and contributors. Adds git_context to output.",
428
462
  ),
429
463
  git_depth: int = typer.Option(
430
464
  20,
431
465
  "--git-depth",
432
- help="Numero de commits recientes a incluir con --git-context (default: 20)",
466
+ help="Number of recent commits to include with --git-context (default: 20, max: 100).",
433
467
  min=1,
434
468
  max=100,
435
469
  ),
436
470
  git_days: int = typer.Option(
437
471
  90,
438
472
  "--git-days",
439
- help="Ventana temporal en dias para detectar ficheros mas activos con --git-context (default: 90)",
473
+ help="Time window in days for detecting change hotspots with --git-context (default: 90). Hotspots are files with the most commits in this window.",
440
474
  min=1,
441
475
  max=3650,
442
476
  ),
443
477
  env_map: bool = typer.Option(
444
478
  False,
445
479
  "--env-map",
446
- help="Incluir mapa de variables de entorno: claves, tipos, categorias y ficheros que las referencian",
480
+ help="Map environment variables: keys, types (string/int/bool/url/path), categories (database/auth/service/...), and which files reference them. Adds env_map and env_summary.",
447
481
  ),
448
482
  code_notes: bool = typer.Option(
449
483
  False,
450
484
  "--code-notes",
451
- help="Extraer anotaciones TODO/FIXME/HACK/NOTE/DEPRECATED/WARNING/BUG/XXX/OPTIMIZE con ubicacion y simbolo envolvente, y detectar ADRs en docs/decisions/, docs/adr/ y similares",
485
+ help=(
486
+ "Extract inline annotations: TODO, FIXME, HACK, NOTE, DEPRECATED, WARNING, BUG, XXX, OPTIMIZE — "
487
+ "with file location and enclosing symbol. "
488
+ "Also detects Architecture Decision Records (ADRs) in docs/decisions/, docs/adr/, and similar paths."
489
+ ),
452
490
  ),
453
491
  agent: bool = typer.Option(
454
492
  False,
455
493
  "--agent",
456
- help="Modo agente: output estructurado y sin ruido para consumo por IA. Incluye identidad, entrypoints, arquitectura, dependencias clave, señales operacionales y gaps. Sin arbol de ficheros ni secciones vacias.",
494
+ help=(
495
+ "Agent-optimized output: structured, noise-free JSON for AI consumption. "
496
+ "Automatically enables --dependencies, --env-map, and --code-notes. Suppresses file tree. "
497
+ "Output includes: identity, entry points, architecture, runtime dependencies, "
498
+ "operational signals, confidence summary, and analysis gaps. No empty sections."
499
+ ),
457
500
  ),
458
501
  trace_pipeline: bool = typer.Option(
459
502
  False,
460
503
  "--trace-pipeline",
461
- help="Modo trazabilidad: incluye pipeline_trace con candidatos, filtros, descartes y origen de cada dato. Para diagnóstico de contaminación de resultados.",
504
+ help=(
505
+ "Diagnostic mode: include pipeline_trace in output showing every candidate, filter decision, "
506
+ "and data origin across all pipeline stages. "
507
+ "Use to diagnose unexpected or contaminated results. Not intended for normal agent use."
508
+ ),
462
509
  ),
463
510
  ) -> None:
464
511
  """Analyze a repository and produce structured context for AI coding agents.
@@ -480,22 +527,22 @@ def main(
480
527
 
481
528
  _t0 = time.monotonic()
482
529
 
483
- # Validar formato
530
+ # Validate format choices
484
531
  if format not in FORMAT_CHOICES:
485
532
  typer.echo(
486
- f"Error: valor invalido '{format}' para --format. Opciones: {', '.join(FORMAT_CHOICES)}",
533
+ f"Error: invalid value '{format}' for --format. Valid options: {', '.join(FORMAT_CHOICES)}",
487
534
  err=True,
488
535
  )
489
536
  raise typer.Exit(code=1)
490
537
  if graph_detail not in GRAPH_DETAIL_CHOICES:
491
538
  typer.echo(
492
- f"Error: valor invalido '{graph_detail}' para --graph-detail. Opciones: {', '.join(GRAPH_DETAIL_CHOICES)}",
539
+ f"Error: invalid value '{graph_detail}' for --graph-detail. Valid options: {', '.join(GRAPH_DETAIL_CHOICES)}",
493
540
  err=True,
494
541
  )
495
542
  raise typer.Exit(code=1)
496
543
  if docs_depth not in DOCS_DEPTH_CHOICES:
497
544
  typer.echo(
498
- f"Error: valor invalido '{docs_depth}' para --docs-depth. Opciones: {', '.join(DOCS_DEPTH_CHOICES)}",
545
+ f"Error: invalid value '{docs_depth}' for --docs-depth. Valid options: {', '.join(DOCS_DEPTH_CHOICES)}",
499
546
  err=True,
500
547
  )
501
548
  raise typer.Exit(code=1)
@@ -509,7 +556,7 @@ def main(
509
556
  typer.echo(f"Error: '{target}' is not a directory.", err=True)
510
557
  raise typer.Exit(code=1)
511
558
 
512
- # --- Importar modulos de logica ---
559
+ # --- Import analysis modules ---
513
560
  from dataclasses import asdict, replace
514
561
 
515
562
  from sourcecode.dependency_analyzer import DependencyAnalyzer
@@ -532,17 +579,17 @@ def main(
532
579
  from sourcecode.serializer import agent_view, compact_view, normalize_source_map, standard_view, validate_cross_analyzer_consistency, validate_source_map, write_output
533
580
  from sourcecode.workspace import WorkspaceAnalyzer
534
581
 
535
- # 1. Escanear el directorio (SCAN-01 a SCAN-05)
582
+ # 1. Scan directory (SCAN-01 to SCAN-05)
536
583
  redactor = SecretRedactor(enabled=not no_redact)
537
584
 
538
- # Detectar manifests antes del scan para ajustar depth.
539
- # find_manifests() solo mira profundidad 0-1, no necesita el arbol.
585
+ # Detect manifests before scan to adjust depth.
586
+ # find_manifests() only looks at depth 0-1, does not need the full tree.
540
587
  _pre_scanner = FileScanner(target, max_depth=1)
541
588
  manifests = _pre_scanner.find_manifests()
542
589
 
543
- # Maven usa src/main/java/<groupId>/<artifactId>/<module>/ (profundidad 7+).
544
- # Con depth=4 los ficheros .java son invisibles y todos los analizadores fallan.
545
- # Necesitamos al menos 8: src(1)+main(2)+java(3)+com(4)+co(5)+app(6)+module(7)+file.
590
+ # Maven uses src/main/java/<groupId>/<artifactId>/<module>/ (depth 7+).
591
+ # At depth=4 Java files are invisible and all analyzers fail.
592
+ # Require at least 8: src(1)+main(2)+java(3)+com(4)+co(5)+app(6)+module(7)+file.
546
593
  _java_manifest_names = {"pom.xml", "build.gradle", "build.gradle.kts"}
547
594
  _is_java = any(Path(m).name in _java_manifest_names for m in manifests)
548
595
  _java_min_depth = 8
@@ -559,12 +606,12 @@ def main(
559
606
  scanner = FileScanner(target, max_depth=effective_depth)
560
607
  raw_tree = scanner.scan_tree()
561
608
 
562
- # 2. Filtrar del arbol las entradas de .env y *.secret (SEC-02, todos los niveles)
609
+ # 2. Filter .env and *.secret entries from file tree (SEC-02, all levels)
563
610
  def filter_sensitive_files(tree: dict[str, Any]) -> dict[str, Any]:
564
611
  filtered: dict[str, Any] = {}
565
612
  for name, value in tree.items():
566
613
  if redactor.should_exclude_file(name):
567
- continue # excluir .env, *.secret del arbol
614
+ continue # exclude .env, *.secret from tree
568
615
  if isinstance(value, dict):
569
616
  filtered[name] = filter_sensitive_files(value)
570
617
  else:
@@ -614,8 +661,8 @@ def main(
614
661
  invalid_edges = sorted(parsed_graph_edges - GRAPH_EDGE_CHOICES)
615
662
  if invalid_edges:
616
663
  typer.echo(
617
- "Error: valores invalidos para --graph-edges: "
618
- f"{', '.join(invalid_edges)}. Opciones: {', '.join(sorted(GRAPH_EDGE_CHOICES))}",
664
+ f"Error: invalid values for --graph-edges: "
665
+ f"{', '.join(invalid_edges)}. Valid options: {', '.join(sorted(GRAPH_EDGE_CHOICES))}",
619
666
  err=True,
620
667
  )
621
668
  raise typer.Exit(code=1)
@@ -804,7 +851,7 @@ def main(
804
851
  else None
805
852
  )
806
853
 
807
- # 3. Construir el schema
854
+ # 3. Build schema
808
855
  # Compute analyzer fingerprints: short hashes of each analyzer's key rule
809
856
  # constants so that a rule change is always visible in the output, regardless
810
857
  # of whether the semver was bumped.
@@ -850,9 +897,29 @@ def main(
850
897
  ),
851
898
  workspace=ws.path,
852
899
  )
853
- all_sem_calls.extend(ws_calls)
854
- all_sem_symbols.extend(ws_syms)
855
- all_sem_links.extend(ws_links)
900
+ # Prefix paths to repo-root-relative (sm.file_paths uses root-relative)
901
+ _pfx = ws.path.rstrip("/") + "/"
902
+ all_sem_calls.extend(
903
+ replace(c,
904
+ caller_path=_pfx + c.caller_path,
905
+ callee_path=_pfx + c.callee_path,
906
+ )
907
+ for c in ws_calls
908
+ )
909
+ all_sem_symbols.extend(
910
+ replace(s, path=_pfx + s.path) for s in ws_syms
911
+ )
912
+ all_sem_links.extend(
913
+ replace(l,
914
+ importer_path=_pfx + l.importer_path,
915
+ source_path=(
916
+ _pfx + l.source_path
917
+ if l.source_path is not None and not l.is_external
918
+ else l.source_path
919
+ ),
920
+ )
921
+ for l in ws_links
922
+ )
856
923
  all_sem_summaries.append(ws_sum)
857
924
  merged_sem = semantic_analyzer.merge_summaries(all_sem_summaries)
858
925
  sm = replace(
@@ -882,27 +949,96 @@ def main(
882
949
  [ws.path for ws in workspace_analysis.workspaces],
883
950
  )
884
951
 
885
- # Phase 9: LLM Output Quality — poblar campos derivados
952
+ # Phase 9: LLM Output Quality — populate derived fields
886
953
  from sourcecode.architecture_summary import ArchitectureSummarizer
887
954
  from sourcecode.summarizer import ProjectSummarizer
888
955
  from sourcecode.tree_utils import flatten_file_tree
889
956
 
890
- # LQN-01: lista plana de paths del file_tree con separador forward-slash
957
+ # LQN-01: flat path list from file_tree with forward-slash separator
891
958
  sm.file_paths = [
892
959
  p.replace("\\", "/") for p in flatten_file_tree(sm.file_tree)
893
960
  ]
894
961
 
895
- # LQN-05: top-15 dependencias directas de manifest/lockfile, ordenadas por rol
962
+ # Semantic hotspots + coverage (needs sm.file_paths populated above)
963
+ if semantic_analyzer is not None and sm.semantic_summary is not None:
964
+ from collections import Counter as _Counter
965
+ from pathlib import Path as _Path
966
+
967
+ _SRC_EXTS = {".py", ".ts", ".tsx", ".js", ".jsx", ".go", ".java", ".kt", ".rs", ".rb"}
968
+ _fan_in: _Counter[str] = _Counter()
969
+ _fan_out: _Counter[str] = _Counter()
970
+ for _call in sm.semantic_calls:
971
+ if _call.callee_path:
972
+ _fan_in[_call.callee_path] += 1
973
+ if _call.caller_path:
974
+ _fan_out[_call.caller_path] += 1
975
+
976
+ _all_call_files = set(_fan_in) | set(_fan_out)
977
+ _hotspots: list[dict] = []
978
+ # Filter test paths from hotspots — they dominate fan-in by calling many modules
979
+ _TEST_MARKERS = {"/test", "/tests", "/spec", "/specs", "_test.", ".test.", ".spec."}
980
+ for _p in _all_call_files:
981
+ if any(_m in _p for _m in _TEST_MARKERS) or _p.startswith("test"):
982
+ continue
983
+ _in = _fan_in[_p]
984
+ _out = _fan_out[_p]
985
+ _score = _in * 2.0 + _out * 1.0
986
+ if _score < 2:
987
+ continue
988
+ if _in > _out * 2:
989
+ _reason = "high fan-in: many callers depend on this"
990
+ elif _out > _in * 2:
991
+ _reason = "high fan-out: orchestrates many modules"
992
+ else:
993
+ _reason = "hub: balanced import/call traffic"
994
+ # Determine method confidence from calls touching this path
995
+ _method = next(
996
+ (c.method for c in sm.semantic_calls if c.callee_path == _p or c.caller_path == _p),
997
+ "heuristic",
998
+ )
999
+ _hotspots.append({
1000
+ "path": _p,
1001
+ "importance_score": round(_score, 1),
1002
+ "fan_in": _in,
1003
+ "fan_out": _out,
1004
+ "reason": _reason,
1005
+ "confidence": "medium" if _method == "heuristic" else "high",
1006
+ })
1007
+ _hotspots.sort(key=lambda x: -x["importance_score"])
1008
+
1009
+ _total_src = sum(
1010
+ 1 for _fp in sm.file_paths
1011
+ if _Path(_fp).suffix.lower() in _SRC_EXTS
1012
+ )
1013
+ _analyzed = sm.semantic_summary.files_analyzed
1014
+ _cov_pct = round(_analyzed / _total_src * 100, 1) if _total_src > 0 else 0.0
1015
+ _cov_conf = (
1016
+ "high" if _cov_pct >= 80
1017
+ else "medium" if _cov_pct >= 40
1018
+ else "low"
1019
+ )
1020
+
1021
+ sm = replace(
1022
+ sm,
1023
+ semantic_summary=replace(
1024
+ sm.semantic_summary,
1025
+ hotspots=_hotspots[:10],
1026
+ coverage_pct=_cov_pct,
1027
+ coverage_confidence=_cov_conf,
1028
+ ),
1029
+ )
1030
+
1031
+ # LQN-05: top-15 direct dependencies from manifest/lockfile, sorted by role
896
1032
  if dependency_analyzer is not None:
897
1033
  from sourcecode.dependency_analyzer import _ROLE_PRIORITY
898
1034
 
899
- primary_ecosystem = sm.stacks[0].stack if sm.stacks else ""
900
- direct_deps = [
901
- d for d in sm.dependencies
902
- if d.scope != "transitive" and d.source in {"manifest", "lockfile"}
903
- and (d.role or "unknown") in {"runtime", "parsing", "serialization", "observability", "infra"}
904
- and d.scope not in {"dev"}
905
- ]
1035
+ primary_ecosystem = sm.stacks[0].stack if sm.stacks else ""
1036
+ direct_deps = [
1037
+ d for d in sm.dependencies
1038
+ if d.scope != "transitive" and d.source in {"manifest", "lockfile"}
1039
+ and (d.role or "unknown") in {"runtime", "parsing", "serialization", "observability", "infra"}
1040
+ and d.scope not in {"dev"}
1041
+ ]
906
1042
 
907
1043
  def _dep_sort_key(d: Any) -> tuple[int, int, str]:
908
1044
  role_order = _ROLE_PRIORITY.get(d.role or "runtime", 5)
@@ -911,14 +1047,14 @@ def main(
911
1047
 
912
1048
  sm.key_dependencies = sorted(direct_deps, key=_dep_sort_key)[:15]
913
1049
 
914
- # LQN-02: resumen NL deterministico
1050
+ # LQN-02: deterministic NL summary
915
1051
  sm.project_summary = ProjectSummarizer(target).generate(sm)
916
1052
  sm.architecture_summary = ArchitectureSummarizer(target).generate(sm)
917
1053
 
918
1054
  # Phase 13 Plan 04: Architectural Inference (--architecture flag)
919
1055
  if architecture:
920
1056
  from sourcecode.architecture_analyzer import ArchitectureAnalyzer
921
- arch_graph = module_graph # None si --graph-modules no fue pasado
1057
+ arch_graph = module_graph # None if --graph-modules was not passed
922
1058
  sm.architecture = ArchitectureAnalyzer().analyze(target, sm, arch_graph)
923
1059
 
924
1060
  # Git Context (--git-context flag)
@@ -982,13 +1118,13 @@ def main(
982
1118
  "example", "examples", "docs", "doc", "fixtures", "fixture",
983
1119
  })
984
1120
  for _ep in sm.entry_points:
985
- _normalized_ep = normalize_entry_point(_ep)
986
- _ep_type = _normalized_ep.entrypoint_type
987
- _path_parts = _ep.path.replace("\\", "/").lower().split("/")
988
- _filtered = (
989
- _normalized_ep.classification != "production"
990
- or any(p in _aux_parts for p in _path_parts)
991
- )
1121
+ _normalized_ep = normalize_entry_point(_ep)
1122
+ _ep_type = _normalized_ep.entrypoint_type
1123
+ _path_parts = _ep.path.replace("\\", "/").lower().split("/")
1124
+ _filtered = (
1125
+ _normalized_ep.classification != "production"
1126
+ or any(p in _aux_parts for p in _path_parts)
1127
+ )
992
1128
  if _filtered:
993
1129
  _trace.emit("output", "agent_view", "filter_ep",
994
1130
  target=_ep.path,
@@ -1004,7 +1140,7 @@ def main(
1004
1140
  ))
1005
1141
  sm = _replace(sm, pipeline_trace=_trace.build_trace())
1006
1142
 
1007
- # 4. Serializar
1143
+ # 4. Serialize
1008
1144
  if agent:
1009
1145
  data = agent_view(sm)
1010
1146
  if not no_redact:
@@ -1058,7 +1194,7 @@ def main(
1058
1194
  except Exception:
1059
1195
  pass
1060
1196
 
1061
- # 6. Escribir output (CLI-04)
1197
+ # 6. Write output (CLI-04)
1062
1198
  write_output(content, output=output)
1063
1199
 
1064
1200
 
@@ -1140,7 +1276,7 @@ def prepare_context_cmd(
1140
1276
 
1141
1277
  target = path.resolve()
1142
1278
  if not target.exists() or not target.is_dir():
1143
- typer.echo(f"Error: '{target}' no es un directorio válido.", err=True)
1279
+ typer.echo(f"Error: '{target}' is not a valid directory.", err=True)
1144
1280
  raise typer.Exit(code=1)
1145
1281
 
1146
1282
  if dry_run: