Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions src/socrates120x/timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,18 @@ def _sprint_events(project: Path) -> list[TimelineEvent]:
return events


_DATED_DECISION = re.compile(r"\((\d{4}-\d{2}-\d{2})\)")
# The date stamp `socrates decide` and `_decisions_md` both emit is
# anchored at the END of the bullet, immediately before the closing
# `**` and optional trailing whitespace. Anchoring here prevents a
# user-typed date in the decision body (e.g. "Migrate by (2024-12-31)
# (2026-05-20)") from being misread as the recording date — the
# previous unanchored `\((\d{4}-\d{2}-\d{2})\)` regex took the FIRST
# match in the line.
_DATED_DECISION_END = re.compile(r"\((\d{4}-\d{2}-\d{2})\)\*{0,2}\s*$")
# Fallback: any (YYYY-MM-DD) anywhere in the line, in case the line
# does NOT end in the canonical `)**` (older files, hand-edited
# bullets). Used only if the anchored match fails.
_DATED_DECISION_ANY = re.compile(r"\((\d{4}-\d{2}-\d{2})\)")


def _decision_events(project: Path) -> list[TimelineEvent]:
Expand All @@ -141,15 +152,17 @@ def _decision_events(project: Path) -> list[TimelineEvent]:
stripped = line.lstrip()
if not stripped.startswith("- "):
continue
m = _DATED_DECISION.search(stripped)
m = _DATED_DECISION_END.search(stripped) or _DATED_DECISION_ANY.search(stripped)
if not m:
continue
try:
d = _dt.date.fromisoformat(m.group(1))
except ValueError:
continue
content = stripped[2:] # strip "- "
content = _DATED_DECISION.sub("", content).strip()
# Strip ONLY the trailing date stamp (anchored) so dates that appear
# in the body are preserved in the rendered timeline entry.
content = _DATED_DECISION_END.sub("", content).strip()
content = content.strip("*").strip()
events.append(TimelineEvent(
date=d,
Expand Down
47 changes: 47 additions & 0 deletions tests/test_timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,50 @@ def test_format_timeline_renders_events(project: Path) -> None:
text = format_timeline(events, use_color=False)
assert yesterday.isoformat() in text
assert "[journal]" in text


def test_decision_with_user_date_in_body_uses_trailing_recording_date(
project: Path,
) -> None:
"""User decision text can mention a date in parens (e.g. a deadline).
The PREVIOUS unanchored regex took the FIRST date in the line, which
was the user's date, not the date the decision was recorded. Anchor
to the trailing `)**` so the recording date wins."""
decisions = project / "planning" / "DECISIONS.md"
decisions.write_text(
decisions.read_text()
+ "\n\n## Decisions added after init\n\n"
+ "- **Migrate by (2024-12-31) for compliance (2026-05-20)**\n"
)
events = build_timeline(project)
decision_events = [e for e in events if e.kind is EventKind.DECISION]
# The decision must be dated 2026-05-20 (the recording date),
# NOT 2024-12-31 (the user's deadline date inside the bullet text).
matching = [e for e in decision_events if "Migrate by" in e.title]
assert matching, "decision was not detected at all"
assert matching[0].date == _dt.date(2026, 5, 20), (
f"expected recording date 2026-05-20; got {matching[0].date} — "
f"likely picked the user-typed (2024-12-31) at the front of the line"
)
# The user's date in the body should be preserved in the rendered title
# (we only strip the trailing recording stamp).
assert "2024-12-31" in matching[0].title


def test_decision_with_no_trailing_stamp_falls_back_to_any_date(
project: Path,
) -> None:
"""Pre-fix files / hand-edited bullets may have just `(YYYY-MM-DD)`
somewhere in the line with no closing `**`. Still detect them via
the unanchored fallback."""
decisions = project / "planning" / "DECISIONS.md"
decisions.write_text(
decisions.read_text()
+ "\n\n## Decisions added after init\n\n"
+ "- legacy bullet style (2025-03-15)\n"
)
events = build_timeline(project)
decision_events = [e for e in events if e.kind is EventKind.DECISION]
matching = [e for e in decision_events if "legacy bullet" in e.title]
assert matching
assert matching[0].date == _dt.date(2025, 3, 15)
Loading