From a9969d8d90f57489b36d9a48f6014cfb7a424c14 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 11 Sep 2026 13:33:27 +0200 Subject: [PATCH] fix: my own gate was forcing sections to open on "4." MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The clinical view of an article was often one numbered list running from the presenting complaint to discharge. Splitting it into Clinical paths / Diagnosis / Management necessarily divides that list — and because the validator required every numeric token to survive, the restructure kept the original numbering. Sections opened on "2." and "4.". The "3." of a list item is a position, not a fact. It is stripped before numbers are compared, so a restructure is free to renumber; and import renumbers every section's lists from 1 regardless, per indentation level, so a nested list counts independently of its parent. A blank line between items is a loose list, not a new one, and does not reset it. 25 sections across 16 of the 54 already applied were repaired in place. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN --- backend/scripts/mdm_pass.py | 45 +++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/backend/scripts/mdm_pass.py b/backend/scripts/mdm_pass.py index 47cc380..6a8abfb 100644 --- a/backend/scripts/mdm_pass.py +++ b/backend/scripts/mdm_pass.py @@ -44,6 +44,44 @@ REQUIRED = MDM_SECTIONS[:3] MARKER = re.compile(r"\[\[(\d+)\|") NUMBER = re.compile(r"\d+(?:\.\d+)?") +#: The "3." that opens an ordered-list item. It is a position in a list, not a +#: fact, and it necessarily changes when a list is split across sections — so +#: it is stripped before the numbers are compared. Requiring it to survive made +#: the first pass keep the original numbering, which left sections opening on +#: "2." and "4." because the list they came from had been divided. +LIST_MARKER = re.compile(r"^(\s*)\d+([.)])(\s)", re.M) + + +def facts_only(text: str) -> str: + """The text with ordered-list numbering removed, for comparing numbers.""" + return LIST_MARKER.sub(r"\1\2\3", text) + + +def renumber(text: str) -> str: + """Number every ordered-list item in this section from 1. + + A list split across two sections leaves the second starting wherever the + first stopped — "2. Blood cultures" opening the Diagnosis section. Counting + is per indentation level, so a nested list is numbered independently of its + parent, and nothing else resets it: a blank line between items is a loose + list, not a new one, and prose in between does not restart the count. + """ + counters: dict[int, int] = {} + out = [] + for line in text.split("\n"): + match = re.match(r"^(\s*)(\d+)([.)])(\s.*)$", line) + if not match: + out.append(line) + continue + indent, _was, dot, rest = match.groups() + depth = len(indent) + for deeper in [level for level in counters if level > depth]: + del counters[deeper] + counters[depth] = counters.get(depth, 0) + 1 + out.append(f"{indent}{counters[depth]}{dot}{rest}") + return "\n".join(out) + + MIN_RATIO, MAX_RATIO = 0.75, 1.35 @@ -94,7 +132,8 @@ def validate(before: list[dict], after: list[dict]) -> list[str]: if want != got: problems.append(f"cross-references changed (lost {sorted(set(want) - set(got)) or '-'}, " f"added {sorted(set(got) - set(want)) or '-'})") - want_n, got_n = sorted(NUMBER.findall(old_text)), sorted(NUMBER.findall(new_text)) + want_n = sorted(NUMBER.findall(facts_only(old_text))) + got_n = sorted(NUMBER.findall(facts_only(new_text))) if want_n != got_n: problems.append(f"numbers changed (lost {sorted(set(want_n) - set(got_n)) or '-'}, " f"added {sorted(set(got_n) - set(want_n)) or '-'})") @@ -172,7 +211,9 @@ def cmd_import(args) -> int: "id": uuid.uuid4().hex, "slug": slugify(section["title"]), "title": section["title"], - "content": section["content"], + # Each section's lists start at 1, whatever they started at + # in the block they were carved out of. + "content": renumber(section["content"]), "parent_id": None, "variant": "clinical", } for section in after]