1"""DiagramFacets: orthogonal diagram display axes + named presets for ll-loop.

2

3 Replaces the old ``--show-diagrams=main|full|mini`` single-enum with:

4

5 --show-diagrams=<topology-or-preset>

6 topology: layered | neighborhood | inline

7 preset: detailed | summary | clean | local | oneline

8

9 --diagram-edge-labels=on|off (default on)

10 --diagram-state-detail=title|full (default full)

11 --diagram-scope=main|full (default full)

12"""

13

14 from __future__ import annotations

15

16 import argparse

17 from dataclasses import dataclass, replace

18

19 TOPOLOGY_VALUES: frozenset[str] = frozenset({"layered", "neighborhood", "inline"})

20 PRESET_VALUES: frozenset[str] = frozenset(

21 {"detailed", "summary", "clean", "local", "oneline", "slim"}

22)

23

24_LEGACY_HINTS: dict[str, str] = {

25 "main": (

26 "main was renamed: use --show-diagrams=summary "

27 "(path scope is now controlled by --diagram-scope=main)."

28 ),

29 "full": (

30 "full was renamed: use --show-diagrams=detailed "

31 "(full graph is now controlled by --diagram-scope=full)."

32 ),

33 "mini": (

34 "mini was renamed: use --show-diagrams=clean "

35 "(or set --diagram-edge-labels=off --diagram-state-detail=title "

36 "for the underlying primitives)."

37 ),

38}

39

40

41@dataclass(frozen=True)

42 class DiagramFacets:

43 """Resolved, orthogonal diagram display settings.

44

45 ``source`` drives fallback gating in ``_choose_pinned_layout``:

46 - ``"topology"`` — user passed an explicit topology; no auto-degradation.

47 - ``"preset"`` — a named preset was used; fall back through smaller topologies.

48 - ``"default"`` — bare ``--show-diagrams`` flag; same fallback as preset.

49 """

50

51 topology: str # layered | neighborhood | inline

52 edge_labels: bool # True = render edge labels

53 state_detail: str # title | full

54 scope: str # main | full (silently ignored for inline topology)

55 source: str # default | preset | topology

56

57

58 PRESET_EXPANSIONS: dict[str, DiagramFacets] = {

59 "detailed": DiagramFacets("layered", True, "full", "full", "preset"),

60 "summary": DiagramFacets("layered", True, "full", "main", "preset"),

61 "clean": DiagramFacets("layered", False, "title", "main", "preset"),

62 "local": DiagramFacets("neighborhood", True, "title", "main", "preset"),

63 "slim": DiagramFacets("neighborhood", False, "title", "main", "preset"),

64 "oneline": DiagramFacets("inline", True, "title", "full", "preset"),

65}

66

67# Maps topology name to the detail string used by _build_pinned_pane

68 TOPOLOGY_TO_DETAIL: dict[str, str] = {

69 "layered": "full",

70 "neighborhood": "neighborhood",

71 "inline": "single",

72}

73

74

75 def _parse_show_diagrams(value: str) -> str:

76 """argparse ``type=`` validator for ``--show-diagrams``.

77

78 Accepts topology values and preset names; rejects legacy mode strings

79 with a migration hint. The ``const=True`` sentinel (bare flag) bypasses

80 this function entirely — argparse does not call ``type=`` on ``const``.

81 """

82 if value in TOPOLOGY_VALUES | PRESET_VALUES:

83 return value

84 if value in _LEGACY_HINTS:

85 raise argparse.ArgumentTypeError(_LEGACY_HINTS[value])

86 raise argparse.ArgumentTypeError(

87 f"unknown --show-diagrams value {value!r}; "

88 f"choose from topologies {sorted(TOPOLOGY_VALUES)} "

89 f"or presets {sorted(PRESET_VALUES)}"

90 )

91

92

93 def resolve_facets(args: argparse.Namespace) -> DiagramFacets | None:

94 """Resolve argparse Namespace into a ``DiagramFacets``, or ``None`` if the flag was absent.

95

96 Modifier flags (``diagram_edge_labels``, ``diagram_state_detail``,

97 ``diagram_scope``) override the preset expansion only when explicitly set

98 (non-None), so preset defaults are preserved when modifiers are absent.

99 """

100 raw = getattr(args, "show_diagrams", None)

101 if raw is None:

102 return None

103

104 if raw is True: # bare --show-diagrams (const=True sentinel)

105 base = replace(PRESET_EXPANSIONS["summary"], source="default")

106 elif raw in PRESET_EXPANSIONS:

107 base = PRESET_EXPANSIONS[raw]

108 elif raw in TOPOLOGY_VALUES:

109 base = DiagramFacets(raw, True, "full", "full", "topology")

110 else:

111 raise ValueError(f"unreachable: argparse should have rejected {raw!r}")

112

113 # Apply modifier overrides — only when the user explicitly set them (not None).

114 edge_labels_raw = getattr(args, "diagram_edge_labels", None)

115 edge_labels = (edge_labels_raw != "off") if edge_labels_raw is not None else base.edge_labels

116

117 state_detail_raw = getattr(args, "diagram_state_detail", None)

118 state_detail = state_detail_raw if state_detail_raw is not None else base.state_detail

119

120 scope_raw = getattr(args, "diagram_scope", None)

121 scope = scope_raw if scope_raw is not None else base.scope

122

123 return replace(base, edge_labels=edge_labels, state_detail=state_detail, scope=scope)