Fix goals audit to preserve prose and support partial goals
GoalsSaver changes: - Now preserves prose content (Vision, Principles, etc.) before goal sections - Only rewrites ## Active, ## Future, ## Non-Goals sections - Extracts and reinserts prose content between frontmatter and goals Audit handler changes: - Supports new status field: "met", "partial", "not_met" - Maintains backward compatibility with old "checked" boolean format - Sets Goal.partial=True for partially achieved goals - Report shows partial count in summary This fixes a bug where the audit button would delete all prose content from goals.md files that had philosophy/context sections. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
69ec7df308
commit
b084e9e30a
|
|
@ -428,7 +428,12 @@ class MilestonesParser(BaseParser):
|
|||
|
||||
|
||||
class GoalsSaver:
|
||||
"""Save goals back to goals.md file."""
|
||||
"""Save goals back to goals.md file.
|
||||
|
||||
This saver preserves prose content (Vision, Principles, etc.) that appears
|
||||
before the goal sections. Only the ## Active, ## Future, and ## Non-Goals
|
||||
sections are rewritten with updated checkbox states.
|
||||
"""
|
||||
|
||||
def __init__(self, path: Path, frontmatter: dict):
|
||||
"""Initialize saver.
|
||||
|
|
@ -441,11 +446,19 @@ class GoalsSaver:
|
|||
self.frontmatter = frontmatter
|
||||
|
||||
def save(self, goal_list: GoalList):
|
||||
"""Save goals back to file.
|
||||
"""Save goals back to file, preserving prose content.
|
||||
|
||||
Args:
|
||||
goal_list: GoalList to save
|
||||
"""
|
||||
# Read existing file to preserve prose content
|
||||
existing_content = ""
|
||||
if self.path.exists():
|
||||
existing_content = self.path.read_text()
|
||||
|
||||
# Extract prose content (everything before first goal section)
|
||||
prose_content = self._extract_prose_content(existing_content)
|
||||
|
||||
lines = []
|
||||
|
||||
# Write frontmatter
|
||||
|
|
@ -454,8 +467,13 @@ class GoalsSaver:
|
|||
lines.append(f"{key}: {value}")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
lines.append("# Goals")
|
||||
lines.append("")
|
||||
|
||||
# Add preserved prose content (without frontmatter, it's already added)
|
||||
if prose_content:
|
||||
lines.append(prose_content)
|
||||
# Ensure there's a blank line before goal sections
|
||||
if not prose_content.endswith("\n\n"):
|
||||
lines.append("")
|
||||
|
||||
# Active goals
|
||||
lines.append("## Active")
|
||||
|
|
@ -488,6 +506,53 @@ class GoalsSaver:
|
|||
|
||||
atomic_write(self.path, "\n".join(lines))
|
||||
|
||||
def _extract_prose_content(self, content: str) -> str:
|
||||
"""Extract prose content before goal sections.
|
||||
|
||||
Preserves everything between the frontmatter and the first goal section
|
||||
(## Active, ## Future, or ## Non-Goals).
|
||||
|
||||
Args:
|
||||
content: Full file content
|
||||
|
||||
Returns:
|
||||
Prose content without frontmatter, or empty string
|
||||
"""
|
||||
if not content:
|
||||
return ""
|
||||
|
||||
# Remove frontmatter
|
||||
lines = content.split("\n")
|
||||
start_idx = 0
|
||||
|
||||
# Skip frontmatter (between --- markers)
|
||||
if lines and lines[0].strip() == "---":
|
||||
for i, line in enumerate(lines[1:], 1):
|
||||
if line.strip() == "---":
|
||||
start_idx = i + 1
|
||||
break
|
||||
|
||||
# Find where goal sections start
|
||||
goal_section_headers = ["## active", "## future", "## non-goal", "## non goal"]
|
||||
end_idx = len(lines)
|
||||
|
||||
for i, line in enumerate(lines[start_idx:], start_idx):
|
||||
line_lower = line.strip().lower()
|
||||
if any(line_lower.startswith(header) for header in goal_section_headers):
|
||||
end_idx = i
|
||||
break
|
||||
|
||||
# Extract prose content
|
||||
prose_lines = lines[start_idx:end_idx]
|
||||
|
||||
# Strip leading/trailing empty lines but preserve internal structure
|
||||
while prose_lines and not prose_lines[0].strip():
|
||||
prose_lines.pop(0)
|
||||
while prose_lines and not prose_lines[-1].strip():
|
||||
prose_lines.pop()
|
||||
|
||||
return "\n".join(prose_lines)
|
||||
|
||||
@staticmethod
|
||||
def _get_checkbox(goal: Goal) -> str:
|
||||
"""Get the checkbox marker for a goal's state.
|
||||
|
|
|
|||
|
|
@ -2512,29 +2512,53 @@ generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|||
updated=frontmatter.get("updated"),
|
||||
)
|
||||
|
||||
def parse_status(item: dict) -> tuple[bool, bool]:
|
||||
"""Parse status field to (completed, partial) tuple.
|
||||
|
||||
Supports both old format (checked: bool) and new format (status: str).
|
||||
"""
|
||||
# New format: status field
|
||||
status = item.get("status", "").lower()
|
||||
if status == "met":
|
||||
return True, False
|
||||
elif status == "partial":
|
||||
return False, True
|
||||
elif status == "not_met":
|
||||
return False, False
|
||||
|
||||
# Old format fallback: checked field
|
||||
checked = item.get("checked", False)
|
||||
return checked, False
|
||||
|
||||
# Process active goals
|
||||
for item in audit_data.get("active", []):
|
||||
completed, partial = parse_status(item)
|
||||
goal = Goal(
|
||||
text=item.get("text", ""),
|
||||
completed=item.get("checked", False),
|
||||
completed=completed,
|
||||
partial=partial,
|
||||
priority=item.get("priority", "medium"),
|
||||
)
|
||||
goal_list.active.append(goal)
|
||||
|
||||
# Process future goals
|
||||
for item in audit_data.get("future", []):
|
||||
completed, partial = parse_status(item)
|
||||
goal = Goal(
|
||||
text=item.get("text", ""),
|
||||
completed=item.get("checked", False),
|
||||
completed=completed,
|
||||
partial=partial,
|
||||
priority=item.get("priority", "medium"),
|
||||
)
|
||||
goal_list.future.append(goal)
|
||||
|
||||
# Process non-goals
|
||||
for item in audit_data.get("non_goals", []):
|
||||
completed, partial = parse_status(item)
|
||||
goal = Goal(
|
||||
text=item.get("text", ""),
|
||||
completed=item.get("checked", False),
|
||||
completed=completed,
|
||||
partial=partial,
|
||||
priority=item.get("priority", "medium"),
|
||||
)
|
||||
goal_list.non_goals.append(goal)
|
||||
|
|
@ -2545,6 +2569,19 @@ generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|||
|
||||
def _build_audit_report(self, audit_data: dict) -> str:
|
||||
"""Build a human-readable report from audit data."""
|
||||
|
||||
def get_status_marker(item: dict) -> str:
|
||||
"""Get checkbox marker from status or checked field."""
|
||||
status = item.get("status", "").lower()
|
||||
if status == "met":
|
||||
return "[x]"
|
||||
elif status == "partial":
|
||||
return "[~]"
|
||||
elif status == "not_met":
|
||||
return "[ ]"
|
||||
# Fallback to old format
|
||||
return "[x]" if item.get("checked") else "[ ]"
|
||||
|
||||
lines = []
|
||||
lines.append("=" * 60)
|
||||
lines.append("GOALS AUDIT REPORT")
|
||||
|
|
@ -2555,7 +2592,7 @@ generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|||
lines.append("ACTIVE GOALS")
|
||||
lines.append("-" * 40)
|
||||
for item in audit_data.get("active", []):
|
||||
status = "[x]" if item.get("checked") else "[ ]"
|
||||
status = get_status_marker(item)
|
||||
lines.append(f"{status} {item.get('text', '')}")
|
||||
lines.append(f" -> {item.get('reason', 'No reason provided')}")
|
||||
lines.append("")
|
||||
|
|
@ -2564,7 +2601,7 @@ generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|||
lines.append("FUTURE GOALS")
|
||||
lines.append("-" * 40)
|
||||
for item in audit_data.get("future", []):
|
||||
status = "[x]" if item.get("checked") else "[ ]"
|
||||
status = get_status_marker(item)
|
||||
lines.append(f"{status} {item.get('text', '')}")
|
||||
lines.append(f" -> {item.get('reason', 'No reason provided')}")
|
||||
lines.append("")
|
||||
|
|
@ -2573,7 +2610,7 @@ generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|||
lines.append("NON-GOALS")
|
||||
lines.append("-" * 40)
|
||||
for item in audit_data.get("non_goals", []):
|
||||
status = "[x]" if item.get("checked") else "[ ]"
|
||||
status = get_status_marker(item)
|
||||
lines.append(f"{status} {item.get('text', '')}")
|
||||
lines.append(f" -> {item.get('reason', 'No reason provided')}")
|
||||
lines.append("")
|
||||
|
|
@ -2583,7 +2620,13 @@ generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
|
|||
lines.append("=" * 60)
|
||||
lines.append("SUMMARY")
|
||||
lines.append("=" * 60)
|
||||
lines.append(f"Active Goals: {summary.get('active_met', 0)}/{summary.get('active_total', 0)} met")
|
||||
active_met = summary.get('active_met', 0)
|
||||
active_partial = summary.get('active_partial', 0)
|
||||
active_total = summary.get('active_total', 0)
|
||||
if active_partial > 0:
|
||||
lines.append(f"Active Goals: {active_met} met, {active_partial} partial / {active_total} total")
|
||||
else:
|
||||
lines.append(f"Active Goals: {active_met}/{active_total} met")
|
||||
lines.append(f"Future Goals: {summary.get('future_started', 0)}/{summary.get('future_total', 0)} started")
|
||||
lines.append(f"Non-Goals: {summary.get('non_goals_confirmed', 0)}/{summary.get('non_goals_total', 0)} confirmed")
|
||||
lines.append("")
|
||||
|
|
|
|||
Loading…
Reference in New Issue