sourcecode 2.4.0__py3-none-any.whl → 2.5.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
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "2.4.0"
7
+ __version__ = "2.5.0"
sourcecode/archetype.py CHANGED
@@ -80,6 +80,17 @@ _MIN_PRIMARY_SCORE = 0.05
80
80
  # A winner must clear this absolute score (not just the top-2 margin) to earn
81
81
  # medium/high confidence — a near-floor lone candidate stays "low".
82
82
  _MIN_CONFIDENT_SCORE = 0.5
83
+ # Fraction of engine's positive contribution discounted for a non-runnable artifact
84
+ # (P0 library de-bias): an engine-like internal core is expected of a library, so it is not
85
+ # discriminating. Tuned so a clear library (no app/server/servlet entry) is not confidently
86
+ # labeled an engine, while runnable systems (which never trigger the gate) are untouched.
87
+ _LIBRARY_ENGINE_DISCOUNT = 0.55
88
+ # Entry kinds that make a repo a runnable system (an app/server/servlet/daemon) rather
89
+ # than a consumable library. Used to gate the library de-bias (P0).
90
+ _RUNNABLE_ENTRY_KINDS = frozenset({
91
+ "application", "bootstrap", "main", "server", "daemon", "worker",
92
+ "mvc_controller", "jax_rs_controller", "servlet", "security_filter", "filter",
93
+ })
83
94
 
84
95
 
85
96
  @dataclass
@@ -389,6 +400,41 @@ class ArchetypeClassifier:
389
400
  f"{f.module_count} modules; distributed mass ≈ {cm['distributed']:.0%}",
390
401
  1.5, multi, max(cm["distributed"], 0.1))
391
402
  self._apply_graph_evidence("software_archetype", cands, f)
403
+ # ── Library de-bias (P0) ────────────────────────────────────────────────
404
+ # A consumable JVM artifact — `jar` packaging with no server/bootstrap/application
405
+ # entry — is a library, not a runnable system. But a library naturally exhibits
406
+ # engine-like topology: a few core API types with high fan-in, tightly-coupled
407
+ # internals (SCCs), hub concentration. Those graph metrics (`graph_*`) otherwise
408
+ # label every library an "engine" (observed: jobrunr → engine(high), library 0.3).
409
+ # Correct it transparently: (1) add a packaging-grounded library signal that does
410
+ # not depend on an `api/` package existing; (2) append a NAMED negative evidence row
411
+ # that discounts most of engine's graph-derived score, since topological hubness in
412
+ # a library is expected and therefore not discriminating for "engine".
413
+ # A runnable/web entry of ANY kind (an app bootstrap, an HTTP controller, a
414
+ # servlet, a daemon) means the repo is a system you run, not a library you depend
415
+ # on. Packaging is unreliable here (gradle libs report no packaging; maven
416
+ # aggregators report `pom`), so the entry-kind surface is the robust signal.
417
+ is_runnable = any(k in f.entry_kinds for k in _RUNNABLE_ENTRY_KINDS)
418
+ is_library_shaped = not is_runnable
419
+ if is_library_shaped:
420
+ add("library", "no_runnable_entry",
421
+ f"entry kinds {sorted(f.entry_kinds) or 'none'} — no app/server/servlet "
422
+ "bootstrap; a consumable artifact, not a runnable system",
423
+ 3.0, 1.0, 0.7)
424
+ # A library legitimately owns an engine-like core (storage/kernel mass, hub
425
+ # topology, SCCs) — that is what a library *is*. Discount most of engine's
426
+ # positive evidence for a non-runnable artifact so its internal machinery does
427
+ # not, alone, promote it to "engine". Transparent: a named negative row.
428
+ engine_positive = sum(max(0.0, e.contribution) for e in cands["engine"])
429
+ if engine_positive > 0:
430
+ discount = engine_positive * _LIBRARY_ENGINE_DISCOUNT
431
+ cands["engine"].append(Evidence(
432
+ "library_core_discount",
433
+ "non-runnable artifact: an engine-like internal core (storage/kernel "
434
+ "mass, hubs, SCCs) is expected of a library and is not, alone, evidence "
435
+ "of an engine product",
436
+ 1.0, 1.0, 1.0, -discount,
437
+ ))
392
438
  return self._resolve("software_archetype", cands, f)
393
439
 
394
440
  def _architectural_style(self, f: _Features) -> DimensionResult:
sourcecode/cli.py CHANGED
@@ -248,6 +248,8 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
248
248
  "chunk-file",
249
249
  # Experimental evidence-based archetype (parallel to legacy)
250
250
  "archetype",
251
+ # Experimental Semantic Retrieval Engine (Layer 3.5)
252
+ "retrieve",
251
253
  }
252
254
  )
253
255
 
@@ -600,6 +602,12 @@ app.add_typer(cache_app, name="cache")
600
602
  auth_app = typer.Typer(help="Authentication: status, logout.", rich_markup_mode="rich")
601
603
  app.add_typer(auth_app, name="auth")
602
604
 
605
+ retrieve_app = typer.Typer(
606
+ help="[EXPERIMENTAL] Semantic Retrieval Engine — typed knowledge queries.",
607
+ rich_markup_mode="rich",
608
+ )
609
+ app.add_typer(retrieve_app, name="retrieve")
610
+
603
611
 
604
612
  def _maybe_show_telemetry_notice() -> None:
605
613
  """Show first-run telemetry notice once, on interactive TTYs only.
@@ -3887,7 +3895,7 @@ def endpoints_cmd(
3887
3895
  as policy "none_detected"):
3888
3896
  {
3889
3897
  "customSecurityAnnotations": [
3890
- {"shortName": "M3FiltroSeguridad",
3898
+ {"shortName": "CustomSecurityAnnotation",
3891
3899
  "resourceParam": "nombreRecurso", "levelParam": "nivelRequerido"}
3892
3900
  ]
3893
3901
  }
@@ -5558,7 +5566,12 @@ def fix_bug_cmd(
5558
5566
  ctx: typer.Context,
5559
5567
  path: Path = typer.Argument(
5560
5568
  Path("."),
5561
- help="Repository root (default: current directory)",
5569
+ help="Repository root (default: current directory), OR the bug symptom when a second path argument follows (sibling-consistent `fix-bug <symptom> <path>`).",
5570
+ ),
5571
+ path2: Optional[Path] = typer.Argument(
5572
+ None,
5573
+ help="Repository root when the first argument is the bug symptom.",
5574
+ show_default=False,
5562
5575
  ),
5563
5576
  symptom: Optional[str] = typer.Option(
5564
5577
  None, "--symptom", "-s",
@@ -5596,7 +5609,16 @@ def fix_bug_cmd(
5596
5609
  ask impact <target> — Propagate impact from a specific class
5597
5610
  ask onboard . — Full architecture context first
5598
5611
  """
5599
- # Detect misuse: `fix-bug "symptom text" /path` — path arg looks like a symptom.
5612
+ # Sibling-consistent positional form: `fix-bug <symptom> <path>` (mirrors
5613
+ # `impact <target> <path>` / `explain <symbol> <path>`). When a second positional is
5614
+ # supplied, the first positional is the symptom and the second is the repository path.
5615
+ if path2 is not None:
5616
+ if not symptom:
5617
+ symptom = str(path)
5618
+ path = path2
5619
+
5620
+ # Detect misuse: `fix-bug "symptom text"` alone — the single positional looks like a
5621
+ # symptom, not a path.
5600
5622
  _path_str = str(path)
5601
5623
  _path_looks_like_symptom = (
5602
5624
  not Path(_path_str).exists()
@@ -6509,6 +6531,410 @@ def archetype_cmd(
6509
6531
  typer.echo(out)
6510
6532
 
6511
6533
 
6534
+ def _run_retrieval_intent(
6535
+ intent_name: str,
6536
+ path: Path,
6537
+ output_path: Optional[Path],
6538
+ trace: bool,
6539
+ anchors: tuple = (),
6540
+ ) -> None:
6541
+ """Shared adapter for the experimental Semantic Retrieval commands. The CLI is one
6542
+ thin surface over the CLI-independent engine: it submits a typed RetrievalRequest and
6543
+ prints the structured JSON result (optionally with the runtime trace/profiling)."""
6544
+ import json as _json
6545
+ from sourcecode.retrieval import (
6546
+ RetrievalAnchor,
6547
+ RetrievalIntent,
6548
+ RetrievalRequest,
6549
+ SemanticRetriever,
6550
+ )
6551
+
6552
+ target = Path(path).resolve()
6553
+ retriever = SemanticRetriever.for_repo(target)
6554
+ request = RetrievalRequest(
6555
+ intent=RetrievalIntent(intent_name),
6556
+ anchors=tuple(RetrievalAnchor(kind=k, raw=v) for k, v in anchors),
6557
+ )
6558
+ run = retriever.run(request)
6559
+ payload: dict = run.result.to_dict()
6560
+ if trace:
6561
+ payload = {"result": payload, "runtime": run.report.to_dict()}
6562
+ out = _json.dumps(payload, indent=2, ensure_ascii=False)
6563
+ if output_path:
6564
+ _safe_write_file(output_path, out)
6565
+ typer.echo(f"Retrieval result written to {output_path}")
6566
+ else:
6567
+ typer.echo(out)
6568
+
6569
+
6570
+ @retrieve_app.command("architectural-core")
6571
+ def retrieve_architectural_core_cmd(
6572
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6573
+ output_path: Optional[Path] = typer.Option(
6574
+ None, "--output", "-o", help="Write output to a file instead of stdout."
6575
+ ),
6576
+ trace: bool = typer.Option(
6577
+ False, "--trace", help="Include the Query Plan Runtime trace + per-step profiling."
6578
+ ),
6579
+ ) -> None:
6580
+ """[EXPERIMENTAL] Retrieve the ARCHITECTURAL_CORE characterization as structured JSON.
6581
+
6582
+ A Semantic Retrieval query (Layer 3.5): it consults ONLY existing Knowledge-Layer
6583
+ capabilities (ArchetypeClassifier + GraphEvidenceProvider) — no extraction, no new
6584
+ heuristics, no natural language. Output is fully typed/structured JSON.
6585
+ """
6586
+ _run_retrieval_intent("architectural-core", path, output_path, trace)
6587
+
6588
+
6589
+ @retrieve_app.command("architectural-style")
6590
+ def retrieve_architectural_style_cmd(
6591
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6592
+ output_path: Optional[Path] = typer.Option(
6593
+ None, "--output", "-o", help="Write output to a file instead of stdout."
6594
+ ),
6595
+ trace: bool = typer.Option(
6596
+ False, "--trace", help="Include the Query Plan Runtime trace + per-step profiling."
6597
+ ),
6598
+ ) -> None:
6599
+ """[EXPERIMENTAL] Retrieve the ARCHITECTURAL_STYLE characterization as structured JSON.
6600
+
6601
+ Same Query Plan Runtime as architectural-core — this intent differs only in its
6602
+ declarative plan (the archetype dimension). No runtime code changes to add it.
6603
+ """
6604
+ _run_retrieval_intent("architectural-style", path, output_path, trace)
6605
+
6606
+
6607
+ @retrieve_app.command("deployment-shape")
6608
+ def retrieve_deployment_shape_cmd(
6609
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6610
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6611
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6612
+ ) -> None:
6613
+ """[EXPERIMENTAL] DEPLOYMENT_SHAPE — the deployment-shape archetype dimension.
6614
+ Same plan as architectural-core, different ArchetypeClassifier dimension."""
6615
+ _run_retrieval_intent("deployment-shape", path, output_path, trace)
6616
+
6617
+
6618
+ @retrieve_app.command("primary-interface")
6619
+ def retrieve_primary_interface_cmd(
6620
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6621
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6622
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6623
+ ) -> None:
6624
+ """[EXPERIMENTAL] PRIMARY_INTERFACE — the primary-interface archetype dimension.
6625
+ Same plan as architectural-core, different ArchetypeClassifier dimension."""
6626
+ _run_retrieval_intent("primary-interface", path, output_path, trace)
6627
+
6628
+
6629
+ @retrieve_app.command("blast-radius")
6630
+ def retrieve_blast_radius_cmd(
6631
+ target: str = typer.Option(..., "--target", "-t", help="Symbol (class/FQN) or file to analyze."),
6632
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6633
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6634
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6635
+ ) -> None:
6636
+ """[EXPERIMENTAL] BLAST_RADIUS — structural change impact (dependents + risk) for a
6637
+ target, via the Semantic Retrieval Engine. Reuses `compute_blast_radius`."""
6638
+ _run_retrieval_intent("blast-radius", path, output_path, trace, anchors=(("symbol", target),))
6639
+
6640
+
6641
+ @retrieve_app.command("affected-endpoints")
6642
+ def retrieve_affected_endpoints_cmd(
6643
+ target: str = typer.Option(..., "--target", "-t", help="Symbol (class/FQN) or file to analyze."),
6644
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6645
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6646
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6647
+ ) -> None:
6648
+ """[EXPERIMENTAL] AFFECTED_ENDPOINTS — HTTP endpoints in the blast cone of a target."""
6649
+ _run_retrieval_intent("affected-endpoints", path, output_path, trace, anchors=(("symbol", target),))
6650
+
6651
+
6652
+ @retrieve_app.command("affected-integrations")
6653
+ def retrieve_affected_integrations_cmd(
6654
+ target: str = typer.Option(..., "--target", "-t", help="Symbol (class/FQN) or file to analyze."),
6655
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6656
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6657
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6658
+ ) -> None:
6659
+ """[EXPERIMENTAL] AFFECTED_INTEGRATIONS — external integrations in the blast cone of a
6660
+ target. Reuses `SemanticIntegrationEngine`."""
6661
+ _run_retrieval_intent(
6662
+ "affected-integrations", path, output_path, trace, anchors=(("symbol", target),)
6663
+ )
6664
+
6665
+
6666
+ @retrieve_app.command("transaction-boundaries")
6667
+ def retrieve_transaction_boundaries_cmd(
6668
+ target: str = typer.Option(..., "--target", "-t", help="Symbol (class/FQN) or file to analyze."),
6669
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6670
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6671
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6672
+ ) -> None:
6673
+ """[EXPERIMENTAL] TRANSACTION_BOUNDARIES — transactional boundaries touched by a
6674
+ target's blast cone."""
6675
+ _run_retrieval_intent(
6676
+ "transaction-boundaries", path, output_path, trace, anchors=(("symbol", target),)
6677
+ )
6678
+
6679
+
6680
+ @retrieve_app.command("cross-module-impact")
6681
+ def retrieve_cross_module_impact_cmd(
6682
+ target: str = typer.Option(..., "--target", "-t", help="Symbol (class/FQN) or file to analyze."),
6683
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6684
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6685
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6686
+ ) -> None:
6687
+ """[EXPERIMENTAL] CROSS_MODULE_IMPACT — subsystems/modules hit by a target's change."""
6688
+ _run_retrieval_intent(
6689
+ "cross-module-impact", path, output_path, trace, anchors=(("symbol", target),)
6690
+ )
6691
+
6692
+
6693
+ @retrieve_app.command("transaction-flow")
6694
+ def retrieve_transaction_flow_cmd(
6695
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6696
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6697
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6698
+ ) -> None:
6699
+ """[EXPERIMENTAL] TRANSACTION_FLOW — the @Transactional boundary map. Reuses
6700
+ SpringSemanticModel's transaction index."""
6701
+ _run_retrieval_intent("transaction-flow", path, output_path, trace)
6702
+
6703
+
6704
+ @retrieve_app.command("transaction-entrypoints")
6705
+ def retrieve_transaction_entrypoints_cmd(
6706
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6707
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6708
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6709
+ ) -> None:
6710
+ """[EXPERIMENTAL] TRANSACTION_ENTRYPOINTS — transactional boundaries at the API surface."""
6711
+ _run_retrieval_intent("transaction-entrypoints", path, output_path, trace)
6712
+
6713
+
6714
+ @retrieve_app.command("transaction-propagation")
6715
+ def retrieve_transaction_propagation_cmd(
6716
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6717
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6718
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6719
+ ) -> None:
6720
+ """[EXPERIMENTAL] TRANSACTION_PROPAGATION — non-default propagation points + propagation
6721
+ risks. Reuses the Spring transaction engine."""
6722
+ _run_retrieval_intent("transaction-propagation", path, output_path, trace)
6723
+
6724
+
6725
+ @retrieve_app.command("security-surface")
6726
+ def retrieve_security_surface_cmd(
6727
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6728
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6729
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6730
+ ) -> None:
6731
+ """[EXPERIMENTAL] SECURITY_SURFACE — the security audit surface. Reuses run_security_audit."""
6732
+ _run_retrieval_intent("security-surface", path, output_path, trace)
6733
+
6734
+
6735
+ @retrieve_app.command("security-chain")
6736
+ def retrieve_security_chain_cmd(
6737
+ target: str = typer.Option(..., "--target", "-t", help="Symbol (class/FQN) to scope the chain to."),
6738
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6739
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6740
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6741
+ ) -> None:
6742
+ """[EXPERIMENTAL] SECURITY_CHAIN — security findings scoped to a target symbol."""
6743
+ _run_retrieval_intent("security-chain", path, output_path, trace, anchors=(("symbol", target),))
6744
+
6745
+
6746
+ @retrieve_app.command("validation-points")
6747
+ def retrieve_validation_points_cmd(
6748
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6749
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6750
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6751
+ ) -> None:
6752
+ """[EXPERIMENTAL] VALIDATION_POINTS — request-body validation constraints + gaps.
6753
+ Reuses build_validation_surface."""
6754
+ _run_retrieval_intent("validation-points", path, output_path, trace)
6755
+
6756
+
6757
+ @retrieve_app.command("endpoint-inventory")
6758
+ def retrieve_endpoint_inventory_cmd(
6759
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6760
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6761
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6762
+ ) -> None:
6763
+ """[EXPERIMENTAL] ENDPOINT_INVENTORY — the repository's HTTP surface.
6764
+ Reuses SpringSemanticModel.endpoint_index."""
6765
+ _run_retrieval_intent("endpoint-inventory", path, output_path, trace)
6766
+
6767
+
6768
+ @retrieve_app.command("integration-inventory")
6769
+ def retrieve_integration_inventory_cmd(
6770
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6771
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6772
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6773
+ ) -> None:
6774
+ """[EXPERIMENTAL] INTEGRATION_INVENTORY — every detected external integration.
6775
+ Reuses SemanticIntegrationEngine (whole repo, no cone filter)."""
6776
+ _run_retrieval_intent("integration-inventory", path, output_path, trace)
6777
+
6778
+
6779
+ @retrieve_app.command("event-topology")
6780
+ def retrieve_event_topology_cmd(
6781
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6782
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6783
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6784
+ ) -> None:
6785
+ """[EXPERIMENTAL] EVENT_TOPOLOGY — Spring event publishers/listeners per event type.
6786
+ Reuses SpringSemanticModel.event_graph."""
6787
+ _run_retrieval_intent("event-topology", path, output_path, trace)
6788
+
6789
+
6790
+ @retrieve_app.command("subsystem-inventory")
6791
+ def retrieve_subsystem_inventory_cmd(
6792
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6793
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6794
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6795
+ ) -> None:
6796
+ """[EXPERIMENTAL] SUBSYSTEM_INVENTORY — the repository's subsystem partition.
6797
+ Reuses the repo IR subsystem model."""
6798
+ _run_retrieval_intent("subsystem-inventory", path, output_path, trace)
6799
+
6800
+
6801
+ @retrieve_app.command("subsystem-members")
6802
+ def retrieve_subsystem_members_cmd(
6803
+ subsystem: str = typer.Option(..., "--subsystem", "-s", help="Subsystem label or package prefix."),
6804
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6805
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6806
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6807
+ ) -> None:
6808
+ """[EXPERIMENTAL] SUBSYSTEM_MEMBERS — the member symbols of a subsystem.
6809
+ Reuses the repo IR subsystem model."""
6810
+ _run_retrieval_intent(
6811
+ "subsystem-members", path, output_path, trace, anchors=(("subsystem", subsystem),)
6812
+ )
6813
+
6814
+
6815
+ @retrieve_app.command("module-dependents")
6816
+ def retrieve_module_dependents_cmd(
6817
+ module: str = typer.Option(..., "--module", "-m", help="Module path or package prefix."),
6818
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6819
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6820
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6821
+ ) -> None:
6822
+ """[EXPERIMENTAL] MODULE_DEPENDENTS — modules that depend on the scope.
6823
+ Reuses the module graph (GraphAnalyzer)."""
6824
+ _run_retrieval_intent(
6825
+ "module-dependents", path, output_path, trace, anchors=(("module", module),)
6826
+ )
6827
+
6828
+
6829
+ @retrieve_app.command("module-dependencies")
6830
+ def retrieve_module_dependencies_cmd(
6831
+ module: str = typer.Option(..., "--module", "-m", help="Module path or package prefix."),
6832
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6833
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6834
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6835
+ ) -> None:
6836
+ """[EXPERIMENTAL] MODULE_DEPENDENCIES — modules the scope depends on.
6837
+ Reuses the module graph (GraphAnalyzer)."""
6838
+ _run_retrieval_intent(
6839
+ "module-dependencies", path, output_path, trace, anchors=(("module", module),)
6840
+ )
6841
+
6842
+
6843
+ @retrieve_app.command("type-dependents")
6844
+ def retrieve_type_dependents_cmd(
6845
+ target: str = typer.Option(..., "--target", "-t", help="Type/symbol (simple name or FQN)."),
6846
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6847
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6848
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6849
+ ) -> None:
6850
+ """[EXPERIMENTAL] TYPE_DEPENDENTS — types that import/implement/inject the target.
6851
+ Reuses the repo IR reverse graph."""
6852
+ _run_retrieval_intent(
6853
+ "type-dependents", path, output_path, trace, anchors=(("symbol", target),)
6854
+ )
6855
+
6856
+
6857
+ @retrieve_app.command("endpoint-contract")
6858
+ def retrieve_endpoint_contract_cmd(
6859
+ ref: str = typer.Option(..., "--endpoint", "-e", help="Endpoint path or handler symbol."),
6860
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6861
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6862
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6863
+ ) -> None:
6864
+ """[EXPERIMENTAL] ENDPOINT_CONTRACT — the request/validation contract of an endpoint.
6865
+ Reuses build_validation_surface, scoped to one endpoint."""
6866
+ _run_retrieval_intent(
6867
+ "endpoint-contract", path, output_path, trace, anchors=(("endpoint", ref),)
6868
+ )
6869
+
6870
+
6871
+ @retrieve_app.command("endpoint-security")
6872
+ def retrieve_endpoint_security_cmd(
6873
+ ref: str = typer.Option(..., "--endpoint", "-e", help="Endpoint path or handler symbol."),
6874
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6875
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6876
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6877
+ ) -> None:
6878
+ """[EXPERIMENTAL] ENDPOINT_SECURITY — effective security of an endpoint handler.
6879
+ Reuses the endpoint model's CanonicalSecurity + run_security_audit."""
6880
+ _run_retrieval_intent(
6881
+ "endpoint-security", path, output_path, trace, anchors=(("endpoint", ref),)
6882
+ )
6883
+
6884
+
6885
+ @retrieve_app.command("call-neighborhood")
6886
+ def retrieve_call_neighborhood_cmd(
6887
+ target: str = typer.Option(..., "--target", "-t", help="Symbol (class/FQN/method)."),
6888
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6889
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6890
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6891
+ ) -> None:
6892
+ """[EXPERIMENTAL] CALL_NEIGHBORHOOD — forward callees + reverse callers of a symbol.
6893
+ Reuses SpringSemanticModel.call_adj."""
6894
+ _run_retrieval_intent(
6895
+ "call-neighborhood", path, output_path, trace, anchors=(("symbol", target),)
6896
+ )
6897
+
6898
+
6899
+ @retrieve_app.command("execution-paths-through")
6900
+ def retrieve_execution_paths_cmd(
6901
+ target: str = typer.Option(..., "--target", "-t", help="Symbol (class/FQN/method)."),
6902
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6903
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6904
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6905
+ ) -> None:
6906
+ """[EXPERIMENTAL] EXECUTION_PATHS_THROUGH — multi-hop caller reachability of a symbol.
6907
+ Reuses spring_impact.run_impact_chain."""
6908
+ _run_retrieval_intent(
6909
+ "execution-paths-through", path, output_path, trace, anchors=(("symbol", target),)
6910
+ )
6911
+
6912
+
6913
+ @retrieve_app.command("transactions-reaching")
6914
+ def retrieve_transactions_reaching_cmd(
6915
+ target: str = typer.Option(..., "--target", "-t", help="Symbol (class/FQN/method)."),
6916
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6917
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6918
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6919
+ ) -> None:
6920
+ """[EXPERIMENTAL] TRANSACTIONS_REACHING — @Transactional boundaries that reach a symbol.
6921
+ Reuses run_impact_chain + the tx boundary index."""
6922
+ _run_retrieval_intent(
6923
+ "transactions-reaching", path, output_path, trace, anchors=(("symbol", target),)
6924
+ )
6925
+
6926
+
6927
+ @retrieve_app.command("subsystem-coupling")
6928
+ def retrieve_subsystem_coupling_cmd(
6929
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
6930
+ output_path: Optional[Path] = typer.Option(None, "--output", "-o", help="Write output to a file."),
6931
+ trace: bool = typer.Option(False, "--trace", help="Include the Query Plan Runtime trace."),
6932
+ ) -> None:
6933
+ """[EXPERIMENTAL] SUBSYSTEM_COUPLING — module-graph hubs/cycles/orphans + subsystems.
6934
+ Reuses the module graph summary + the repo IR subsystem partition."""
6935
+ _run_retrieval_intent("subsystem-coupling", path, output_path, trace)
6936
+
6937
+
6512
6938
  @app.command("cold-start")
6513
6939
  def cold_start_cmd(
6514
6940
  path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
@@ -407,7 +407,7 @@ class ContextGraph:
407
407
  __slots__ = (
408
408
  "_cir", "_nodes_by_fqn", "_nodes", "_build_ms",
409
409
  "_body_index", "_literal_index", "_guard_index", "_span_index",
410
- "_class_type_index",
410
+ "_class_type_index", "_relations_all", "_symbols_sorted",
411
411
  )
412
412
 
413
413
  def __init__(self, cir: CanonicalRepositoryIR, *, build_ms: float = 0.0) -> None:
@@ -431,6 +431,14 @@ class ContextGraph:
431
431
  _node_to_symbol(n) for n in raw_nodes
432
432
  )
433
433
  self._nodes_by_fqn: dict[str, Symbol] = {s.fqn: s for s in self._nodes}
434
+ # Memoized full projections (the CIR is immutable for this graph's lifetime).
435
+ # `relations()`/`symbols()` are called once PER FILE by per-file consumers such as
436
+ # `_java_contract_from_graph`; without these caches each call re-converted every
437
+ # edge (`_edge_to_relation`) and re-sorted every node — an O(files × edges) blow-up
438
+ # that made cold `ask` / `ask --agent` take minutes on large repos. Built once,
439
+ # filtered cheaply thereafter.
440
+ self._relations_all: Optional[tuple[Relation, ...]] = None
441
+ self._symbols_sorted: Optional[tuple[Symbol, ...]] = None
434
442
 
435
443
  # -- construction -------------------------------------------------------
436
444
 
@@ -497,19 +505,18 @@ class ContextGraph:
497
505
  set. Results are sorted by FQN for determinism. This is the primitive
498
506
  the role-convenience methods below are thin sugar over.
499
507
  """
500
- out: list[Symbol] = []
501
- for s in self._nodes:
502
- if kind is not None and s.kind != kind:
503
- continue
504
- if role is not None and s.role != role:
505
- continue
506
- if annotated_with is not None and not s.has_annotation(annotated_with):
507
- continue
508
- if name_contains is not None and name_contains not in s.fqn:
509
- continue
510
- out.append(s)
511
- out.sort(key=lambda s: s.fqn)
512
- return out
508
+ if self._symbols_sorted is None:
509
+ self._symbols_sorted = tuple(sorted(self._nodes, key=lambda s: s.fqn))
510
+ # Filter over the memoized fqn-sorted set (filtering preserves that order, so the
511
+ # result is identical to sorting after filtering — but the sort happens once).
512
+ return [
513
+ s
514
+ for s in self._symbols_sorted
515
+ if (kind is None or s.kind == kind)
516
+ and (role is None or s.role == role)
517
+ and (annotated_with is None or s.has_annotation(annotated_with))
518
+ and (name_contains is None or name_contains in s.fqn)
519
+ ]
513
520
 
514
521
  def types(self) -> list[Symbol]:
515
522
  """All class/interface/enum/annotation symbols, sorted by FQN."""
@@ -575,16 +582,17 @@ class ContextGraph:
575
582
  ) -> list[Relation]:
576
583
  """All relations matching the given filters (AND-combined), in the IR's
577
584
  stable edge order (already sorted from → type → to)."""
578
- out: list[Relation] = []
579
- for e in self._cir.call_graph:
580
- if kind is not None and e.get("type") != kind:
581
- continue
582
- if source is not None and e.get("from") != source:
583
- continue
584
- if target is not None and e.get("to") != target:
585
- continue
586
- out.append(_edge_to_relation(e))
587
- return out
585
+ if self._relations_all is None:
586
+ # Convert every edge ONCE; filtered calls then scan the cached Relation
587
+ # objects without re-running `_edge_to_relation` (the cold-path hotspot).
588
+ self._relations_all = tuple(_edge_to_relation(e) for e in self._cir.call_graph)
589
+ return [
590
+ r
591
+ for r in self._relations_all
592
+ if (kind is None or r.kind == kind)
593
+ and (source is None or r.source == source)
594
+ and (target is None or r.target == target)
595
+ ]
588
596
 
589
597
  # -- inheritance / implementation (reuses cir.implementation_graph) ------
590
598
 
@@ -742,7 +742,7 @@ def _extract_symbols(
742
742
  """Phase 1: Extract symbols from a Java source file.
743
743
 
744
744
  extra_capture: extra annotation tokens (e.g. custom security annotations like
745
- "@M3FiltroSeguridad") whose argument lists must be stored in annotation_values
745
+ "@CustomSecurityAnnotation") whose argument lists must be stored in annotation_values
746
746
  even though they are not in the built-in _CAPTURE_ANN_ARGS set.
747
747
 
748
748
  Returns (package, symbols, raw_imports).