Coverage for little_loops / cli / loop / edit_routes.py: 0%

69 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2026-06-26 17:38 -0500

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

2 

3from __future__ import annotations 

4 

5import argparse 

6from pathlib import Path 

7 

8from little_loops.cli.loop._helpers import resolve_loop_path 

9from little_loops.logger import Logger 

10 

11 

12def 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