p = 'backend/app/services/illustrate.py'
s = open(p).read()

# ── the docstring, which no longer describes what happens ────────────────────
head_end = s.index('"""', s.index('"""') + 3) + 3
s = ('''"""A diagram for a section: the model says what, this says how it looks.

Some things are a picture and are taught as prose because prose is what fits
in a database column: a timeline, a branching decision, a table of what
distinguishes four look-alike conditions, the sequence a hormone axis runs in.
A reader remembers the shape of those long after the sentences have gone.

So: the model is shown one section and asked, first, whether a diagram is
genuinely the content here — and only then what should be in it. "No" is the
expected answer for most sections and is not a failure. A picture of a
paragraph is worse than the paragraph.

What comes back is a *spec* — nodes, edges, which step matters — and not SVG.
The first version asked for the drawing itself and got real diagrams and,
about a third of the time, a three-word label hanging out of the bottom of a
box sized for two. A model cannot measure text; `diagram` can, so the geometry
happens there and overflow stops being a matter of luck.

The rendered SVG is still checked before it is stored. It is our own output
now, so the guard should never fire — which is the point of leaving it in.
"""''' + s[head_end:])

# ── the prompt: a spec, not a drawing ────────────────────────────────────────
old_prompt = s[s.index('PROMPT = """'):s.index('def read_reply')]
new_prompt = '''PROMPT = """You are illustrating one section of a pediatric reference article.

Article: {title}
Section: {section}

{body}

Find the one mechanism in this section that is a picture and describe only
that. Not the section: one mechanism from it.

Prefer, in this order, the thing that explains why rather than what:
1. A mechanism — the chain from cause to consequence, with the step that
   matters marked. Why the body does this, not that it does it.
2. A sequence or timeline — what happens in what order, or at what age.
3. A branching decision — the fork, and what sends you down each side.
4. A comparison of things that are confused with each other, side by side on
   the features that separate them.
5. An anatomical relationship, where the anatomy is the explanation.

Most sections are prose and hold no such thing; saying so is the right answer
and costs nothing.

If no diagram belongs here, reply with exactly one line:

USEFUL: no

If one does, reply with a JSON object and nothing else — no prose around it,
no markdown fence:

{{"kind": "flow",
  "title": "Why a child desaturates before an adult does",
  "nodes": [
    {{"id": "a", "label": "Smaller functional residual capacity",
      "note": "Less oxygen stored in the lung"}},
    {{"id": "b", "label": "Higher oxygen consumption",
      "note": "6-8 mL/kg/min against 3-4"}},
    {{"id": "c", "label": "Reserve gone in seconds", "tone": "key"}}
  ],
  "edges": [{{"from": "a", "to": "b"}}, {{"from": "b", "to": "c", "label": "on apnoea"}}],
  "caption": "One full sentence saying what the figure shows."}}

kind is one of:
  flow      a chain from cause to consequence          2 to 8 nodes
  branch    a fork; the first node is the question     3 to 7 nodes
  timeline  what happens when; every node needs "at"   3 to 8 nodes
  cycle     a loop that feeds itself                   3 to 6 nodes
  compare   two or three things side by side           2 to 6 nodes

Every node: "id", "label" (at most 48 characters, and it reads better under
30), optionally "note" (at most 72 characters — the detail under the label),
optionally "at" for a timeline (the age or moment, "6 months", "day 3"), and
optionally "tone":
  "key"   the step that matters — exactly one node per figure, or none
  "bad"   the thing that goes wrong, or the dangerous branch
  "good"  the safe branch or the resolved state
Leave "tone" out for everything else.

Edges are {{"from": id, "to": id}} with an optional "label" of a few words.
For a flow the order of the nodes is the order of the chain, so edges only add
labels. For a branch every edge runs from the first node to one of the others.
Timelines, cycles and comparisons need no edges at all.

Every label a fact from the section above. Correct before it is pretty. Short
labels: the drawing is done by code, and code cannot make three lines of text
fit where one belongs."""


def read_spec(raw: str) -> dict | None:
    """The spec out of a reply, or None when there is nothing usable in it.

    Models put a JSON object inside a fence, after a sentence, or on its own.
    All three are accepted: the object is found by its braces rather than by
    trusting the reply to be exactly what was asked for.
    """
    text = (raw or "").strip()
    if re.match(r"^USEFUL\\s*:\\s*n", text, re.I):
        return {"useful": False}
    at = text.find("{")
    end = text.rfind("}")
    if at == -1 or end <= at:
        return None
    try:
        spec = json.loads(text[at:end + 1])
    except json.JSONDecodeError:
        return None
    if not isinstance(spec, dict):
        return None
    return {"useful": True, "spec": spec}


'''
s = s.replace(old_prompt, new_prompt)

# read_reply is no longer used by the task, but it is still the parser for the
# old plain-text shape and its tests. Left where it is.
s = s.replace('import logging\nimport re', 'import json\nimport logging\nimport re')
open(p, 'w').write(s)
print('written')
