1"""ll-loop edit-routes subcommand: render and edit FSM routing as a decision table."""

2

3 from __future__ import annotations

4

5 import argparse

6 from pathlib import Path

7

8 from little_loops.cli.loop._helpers import resolve_loop_path

9 from little_loops.logger import Logger

10

11

12 def cmd_edit_routes(

13 loop_name: str,

14 args: argparse.Namespace,

15 loops_dir: Path,

16 logger: Logger,

17) -> int:

18 """Render a loop's routing as a decision table; open editor to modify routes.

19

20 Returns:

21 0 = success or no changes

22 1 = parse error or unknown state in edited table

23 2 = loop not found

24 """

25 from little_loops.fsm.route_table import (

26 CompoundGridParser,

27 CompoundGridRenderer,

28 PolicyRuleApplier,

29 PolicyRuleExtractor,

30 RouteTableApplier,

31 RouteTableExtractor,

32 RouteTableParser,

33 RouteTableRenderer,

34 detect_routing_gaps,

35 open_in_editor,

36 )

37 from little_loops.fsm.validation import load_and_validate

38

39 fmt = getattr(args, "format", "markdown")

40 dry_run = getattr(args, "dry_run", False)

41 no_warnings = getattr(args, "no_warnings", False)

42 allow_delete = getattr(args, "allow_delete", False)

43

44 try:

45 path = resolve_loop_path(loop_name, loops_dir)

46 except FileNotFoundError as e:

47 logger.error(str(e))

48 return 2

49

50 try:

51 fsm, _ = load_and_validate(path)

52 except ValueError as e:

53 logger.error(f"{loop_name} is invalid: {e}")

54 return 1

55

56 # Auto-detect decision-table mode: explicit flag OR loop imports policy-router with policy_rules

57 decision_table = getattr(args, "decision_table", False) or (

58 any("lib/policy-router" in imp for imp in fsm.imports) and "policy_rules" in fsm.context

59 )

60

61 if decision_table:

62 # Decision-table mode: compound condition×action grid for policy-router loops

63 rules = PolicyRuleExtractor.extract(fsm)

64 table_text = (

65 CompoundGridRenderer.to_csv(rules)

66 if fmt == "csv"

67 else CompoundGridRenderer.to_markdown(rules)

68 )

69 if dry_run:

70 print(table_text, end="")

71 return 0

72

73 edited = open_in_editor(table_text, fmt)

74 if edited is None:

75 logger.error("Editor exited with non-zero status; no changes applied")

76 return 1

77

78 known_states = set(fsm.states.keys())

79 try:

80 parsed_dt = (

81 CompoundGridParser.parse_csv(edited, known_states)

82 if fmt == "csv"

83 else CompoundGridParser.parse_markdown(edited, known_states)

84 )

85 except ValueError as e:

86 logger.error(f"Parse error: {e}")

87 return 1

88

89 if not no_warnings:

90 for w in parsed_dt.warnings:

91 print(f"⚠ {w}")

92

93 PolicyRuleApplier.apply(path, parsed_dt.rules)

94 return 0

95

96 # Gap/conflict detection (verdict-matrix mode only)

97 if not no_warnings:

98 warnings = detect_routing_gaps(fsm)

99 for w in warnings:

100 print(f"⚠ {w}")

101

102 # Extract and render

103 matrix = RouteTableExtractor.extract(fsm)

104 if fmt == "csv":

105 table_text = RouteTableRenderer.to_csv(matrix)

106 else:

107 table_text = RouteTableRenderer.to_markdown(matrix)

108

109 if dry_run:

110 print(table_text, end="")

111 return 0

112

113 # Open editor

114 edited = open_in_editor(table_text, fmt)

115 if edited is None:

116 logger.error("Editor exited with non-zero status; no changes applied")

117 return 1

118

119 # Parse edited table

120 known_states = set(fsm.states.keys())

121 try:

122 if fmt == "csv":

123 parsed = RouteTableParser.parse_csv(edited, known_states)

124 else:

125 parsed = RouteTableParser.parse_markdown(edited, known_states)

126 except ValueError as e:

127 logger.error(f"Parse error: {e}")

128 return 1

129

130 # Apply changes

131 RouteTableApplier.apply(

132 path, matrix, parsed.matrix, new_stubs=parsed.new_stubs, allow_delete=allow_delete

133 )

134 return 0