1"""Sync decisions log active rules to .ll/ll.local.md."""

2

3 from __future__ import annotations

4

5 from pathlib import Path

6

7 from little_loops.decisions import _DEFAULT_LOG_PATH, RuleEntry, list_entries, resolve_active

8 from little_loops.file_utils import atomic_write

9

10

11 def _resolve_path(path: Path | None) -> Path:

12 return path if path is not None else Path.cwd() / _DEFAULT_LOG_PATH

13

14

15 def sync_to_local_md(path: Path | None = None) -> None:

16 """Write active required rules to ## Active Rules in ll.local.md.

17

18 `path` is the decisions YAML path (e.g. .ll/decisions.yaml).

19 ll.local.md is resolved as path.parent / "ll.local.md".

20 """

21 decisions_path = _resolve_path(path)

22 ll_local_path = decisions_path.parent / "ll.local.md"

23

24 rules = [

25 e

26 for e in list_entries(decisions_path, type="rule")

27 if getattr(e, "enforcement", None) == "required"

28 ]

29 active_rules = resolve_active(rules)

30

31 rules_block = "\n".join(f"- {r.rule}" for r in active_rules if isinstance(r, RuleEntry))

32 section = f"## Active Rules\n\n{rules_block}\n"

33

34 content = ll_local_path.read_text(encoding="utf-8") if ll_local_path.exists() else ""

35 if "## Active Rules" in content:

36 idx = content.rfind("## Active Rules\n")

37 end = content.find("\n##", idx + 1)

38 content = content[:idx] + section + (content[end:] if end != -1 else "")

39 else:

40 content += f"\n\n{section}"

41 atomic_write(ll_local_path, content)