Coverage for little_loops / cli / loop / edit_routes.py: 0%
69 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-29 00:54 -0500
« prev ^ index » next coverage.py v7.12.0, created at 2026-06-29 00:54 -0500
1"""ll-loop edit-routes subcommand: render and edit FSM routing as a decision table."""
3from __future__ import annotations
5import argparse
6from pathlib import Path
8from little_loops.cli.loop._helpers import resolve_loop_path
9from little_loops.logger import Logger
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.
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
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)
44 try:
45 path = resolve_loop_path(loop_name, loops_dir)
46 except FileNotFoundError as e:
47 logger.error(str(e))
48 return 2
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
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 )
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
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
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
89 if not no_warnings:
90 for w in parsed_dt.warnings:
91 print(f"⚠ {w}")
93 PolicyRuleApplier.apply(path, parsed_dt.rules)
94 return 0
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}")
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)
109 if dry_run:
110 print(table_text, end="")
111 return 0
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
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
130 # Apply changes
131 RouteTableApplier.apply(
132 path, matrix, parsed.matrix, new_stubs=parsed.new_stubs, allow_delete=allow_delete
133 )
134 return 0