From e7f1a348f41aadd49702e602ee2e1dc8178f95c5 Mon Sep 17 00:00:00 2001 From: "Aaron K. Clark" Date: Wed, 20 May 2026 16:47:48 -0500 Subject: [PATCH] bugfix(decide): collapse internal whitespace so multi-line text stays one bullet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `record_decision()` formatted the bullet as `- **{text} ({date})**` where text had only outer whitespace stripped. A multi-line decision like socrates decide $'first\nsecond' produced - **first second (2026-05-20)** which terminates the markdown list item at the first newline and leaves an unclosed `**` on its own line — corrupting the DECISIONS.md file. Fix: ' '.join(text.split()) to collapse every run of whitespace (newlines, tabs, multiple spaces) into a single space before formatting. Functionally a no-op for normal single-line inputs. Test: assert a deliberately ugly multi-line input renders as a clean single-line bullet with no stray newlines. 148/148 tests pass; ruff + mypy clean. --- src/socrates120x/decide.py | 6 +++++- tests/test_decide.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/socrates120x/decide.py b/src/socrates120x/decide.py index c246e9a..4e17445 100644 --- a/src/socrates120x/decide.py +++ b/src/socrates120x/decide.py @@ -31,7 +31,11 @@ def record_decision(project: Path, text: str) -> int: ) return 2 - cleaned = text.strip() + # Collapse internal whitespace runs (including newlines, tabs) to single + # spaces. A multi-line decision (`socrates decide $'foo\nbar'`) would + # otherwise produce a bullet whose closing `**` lands on a different + # line, breaking markdown bold rendering and terminating the list item. + cleaned = " ".join(text.split()) if not cleaned: print("error: decision text is empty", file=sys.stderr) return 2 diff --git a/tests/test_decide.py b/tests/test_decide.py index 28887b8..9bb4834 100644 --- a/tests/test_decide.py +++ b/tests/test_decide.py @@ -79,6 +79,25 @@ def test_decide_errors_when_no_decisions_md(tmp_path: Path) -> None: assert code == 2 +def test_decide_collapses_internal_whitespace_to_keep_bullet_single_line( + project: Path, +) -> None: + """A multi-line decision text would otherwise leave the closing `**` + on a different line, breaking the markdown bullet. Collapse all + runs of whitespace to a single space.""" + code = record_decision( + project, + "first line\n second line\t\twith tabs\n\n\nthird line", + ) + assert code == 0 + body = (project / "planning" / "DECISIONS.md").read_text() + today = _dt.date.today().isoformat() + expected_bullet = f"- **first line second line with tabs third line ({today})**" + assert expected_bullet in body + # And the literal multi-line form must NOT appear: + assert "\n second line" not in body + + def test_decide_timeline_picks_up_new_decision(project: Path) -> None: """End-to-end: the date stamp `socrates decide` writes is the one `socrates timeline` reads."""