Files
claude-code df376a0b06
werkator build succeeded after 00:12
werkator/werkrevisor-statistics build succeeded after 00:38
werkrevisor 0.40.1: the README and the diagrams describe the current review (#99)
Co-authored-by: claude-code <claude-code@javagil.de>
2026-10-13 08:30:06 +02:00

124 lines
7.9 KiB
Python

"""Writes docs/werkrevisor-review-decisions.svg and its dark variant: where a review ends early or takes a shortcut."""
from html import escape
LIGHT = dict(bg="#ffffff", text="#141616", muted="#55615f", soft="#3e4645", lane="#f3f6f5", step="#ffffff",
step_line="#c9d3d1", dec="#e7f1f3", dec_line="#0e6b7d", arrow="#6e7c7a", label="#0e6b7d")
DARK = dict(bg="#0d1312", text="#e9efed", muted="#9fb0ad", soft="#b7c4c1", lane="#17201f", step="#1e2927",
step_line="#3a4a47", dec="#132e35", dec_line="#3aa3b8", arrow="#8a9a97", label="#5cc3d6")
OUTCOME = {"exit": "#5f6b75", "reuse": "#0e6b7d", "error": "#58616b", "red": "#c0392b", "green": "#24733e",
"yellow": "#d4a514", "orange": "#e07b2e", "hint": "#3b6fb6"}
# (kind, text lines, exit label, outcome kind, outcome lines)
ROWS = [
("1 Before the model: the pull request and the rules", [
("step", ["Werkator starts", "the review"], None, None, None),
("dec", ["Head current,", "PR open?"], "no", "exit", ["Exit 30,", "writes nothing"]),
("dec", ["Marked ready,", "\"WIP: \" removed?"], "no", "error", ["ERROR"]),
("dec", ["Head reviewed", "before?"], "yes", "reuse", ["Reuses that", "verdict"]),
("step", ["Rules on files", "and diff"], None, None, None),
("dec", ["Open question in", "a PR document?"], "yes", "red", ["RED,", "no model call"]),
("dec", ["Possible secret", "in the diff?"], "yes", "yellow", ["YELLOW, nothing", "to Jev or a model"]),
]),
("2 The shortcut, Jev, the context and the model", [
# The rules still apply, e.g. a risky path keeps its YELLOW.
("dec", ["Only mechanical,", "image or approved?"], "yes", "reuse", ["Verdict of the", "rules alone"]),
("step", ["Jev: PR documents,", "changesets, files;", "PR document check"], None, None, None),
("dec", ["Approved before,", "new blocks safe?"], "yes", "green", ["Approval", "carried over"]),
("dec", ["Classifier mode,", "no model?"], "yes", "green", ["GREEN only if", "Jev judged all files"]),
("dec", ["Prompt within", "the input limit?"], "no", "yellow", ["Excerpts, then diff;", "else YELLOW"]),
("dec", ["Budget left", "on the key?"], "no", "error", ["ERROR,", "no model call"]),
("step", ["Model review:", "advises or decides"], None, None, None),
]),
("3 After the model: findings, verdict and output", [
("dec", ["Model call", "failed?"], "yes", "hint", ["Hint if Jev judged", "all files, else YELLOW"]),
("step", ["Jev judges each", "finding"], "", "orange", ["ORANGE, YELLOW", "or a hint"]),
("step", ["Verdict: the worst", "reason wins; risk and", "finding: RED"], None, None, None),
("dec", ["GREEN without", "full review?"], "yes", "yellow", ["Becomes", "YELLOW"]),
("dec", ["RED for", "max_red_rounds?"], "yes", "red", ["RED, asks the", "human reviewers"]),
("dec", ["Head still", "the same?"], "no", "exit", ["Exit 30,", "writes nothing"]),
("step", ["Comment, status,", "approval, merge;", "usage record"], None, None, None),
]),
]
SPINES = [228, 488, 748]
NODE_W, NODE_H = 176, 84
def svg(c):
out = []
add = out.append
add('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1600 900" width="1600" height="900" role="img" '
'aria-label="Decisions in a werkrevisor review: where it ends early or takes a shortcut">')
add('<title>Decisions in a werkrevisor review</title>')
add(f'''<defs><style>
text {{ font-family: Inter, "IBM Plex Sans", "Segoe UI", Helvetica, Arial, sans-serif; fill: {c["text"]}; }}
.title {{ font-size: 40px; font-weight: 700; letter-spacing: -0.5px; }}
.subtitle {{ font-size: 21px; fill: {c["muted"]}; }}
.row {{ font-size: 15px; font-weight: 700; letter-spacing: 1px; fill: {c["muted"]}; }}
.node {{ font-size: 15.5px; font-weight: 600; }}
.out {{ font-size: 14.5px; font-weight: 600; fill: #ffffff; }}
.out-dark {{ fill: #141616; }}
.yes {{ font-size: 13.5px; font-weight: 700; fill: {c["label"]}; }}
.footer {{ font-size: 14px; fill: {c["muted"]}; }}
</style>
<marker id="a" viewBox="0 0 10 10" refX="8.5" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="{c["arrow"]}"/></marker></defs>''')
add(f'<rect width="1600" height="900" fill="{c["bg"]}"/>')
add('<text class="title" x="40" y="60">Decisions in a werkrevisor review</text>')
add('<text class="subtitle" x="40" y="94">Each check can end the review early or take a shortcut; '
'otherwise the review goes on to the right.</text>')
for r, (heading, nodes) in enumerate(ROWS):
spine = SPINES[r]
add(f'<rect x="30" y="{spine - 92}" width="1540" height="236" rx="16" fill="{c["lane"]}"/>')
add(f'<text class="row" x="48" y="{spine - 64}">{escape(heading.upper())}</text>')
slot = 1500 / len(nodes)
xs = [50 + slot * (i + 0.5) for i in range(len(nodes))]
for i, (kind, lines, exit_label, outcome, outcome_lines) in enumerate(nodes):
x = xs[i]
top = spine - NODE_H / 2
if kind == "dec":
w, h, d = NODE_W, NODE_H, 18
pts = (f"{x - w / 2 + d},{top} {x + w / 2 - d},{top} {x + w / 2},{spine} "
f"{x + w / 2 - d},{top + h} {x - w / 2 + d},{top + h} {x - w / 2},{spine}")
add(f'<polygon points="{pts}" fill="{c["dec"]}" stroke="{c["dec_line"]}" stroke-width="2"/>')
else:
add(f'<rect x="{x - NODE_W / 2}" y="{top}" width="{NODE_W}" height="{NODE_H}" rx="12" '
f'fill="{c["step"]}" stroke="{c["step_line"]}" stroke-width="1.5"/>')
first = spine - (len(lines) - 1) * 10 + 5
for j, line in enumerate(lines):
add(f'<text class="node" x="{x}" y="{first + j * 20}" text-anchor="middle">{escape(line)}</text>')
if i + 1 < len(nodes):
add(f'<path d="M{x + NODE_W / 2},{spine} H{xs[i + 1] - NODE_W / 2 - 2}" fill="none" '
f'stroke="{c["arrow"]}" stroke-width="2.5" marker-end="url(#a)"/>')
if kind == "dec":
opposite = "yes" if exit_label == "no" else "no"
add(f'<text class="yes" x="{x + NODE_W / 2 + 6}" y="{spine - 8}">{opposite}</text>')
if outcome_lines:
ob_top = spine + 64
add(f'<path d="M{x},{spine + NODE_H / 2} V{ob_top - 2}" fill="none" stroke="{OUTCOME[outcome]}" '
f'stroke-width="2.5" marker-end="url(#a)"/>')
add(f'<text class="yes" x="{x + 8}" y="{spine + 57}">{escape(exit_label)}</text>')
add(f'<rect x="{x - 84}" y="{ob_top}" width="168" height="{14 + 19 * len(outcome_lines)}" rx="10" '
f'fill="{OUTCOME[outcome]}"/>')
# Yellow and orange keep their bright color, so their text is dark for the contrast.
cls = "out out-dark" if outcome in ("yellow", "orange") else "out"
for j, line in enumerate(outcome_lines):
add(f'<text class="{cls}" x="{x}" y="{ob_top + 23 + j * 19}" text-anchor="middle">{escape(line)}</text>')
if r + 1 < len(ROWS):
last, nxt = xs[-1], SPINES[r + 1]
mid = spine + 158
add(f'<path d="M{last + NODE_W / 2},{spine} H{last + NODE_W / 2 + 14} V{mid} H{50 + 1500 / len(ROWS[r + 1][1]) / 2} '
f'V{nxt - NODE_H / 2 - 2}" fill="none" stroke="{c["arrow"]}" stroke-width="2.5" stroke-dasharray="7 5" '
f'marker-end="url(#a)"/>')
add('<text class="footer" x="1560" y="890" text-anchor="end">git.javagil.de/mi/werkrevisor</text>')
add('</svg>')
return "\n".join(out) + "\n"
if __name__ == "__main__":
import sys
target = sys.argv[1] if len(sys.argv) > 1 else "docs"
open(f"{target}/werkrevisor-review-decisions.svg", "w").write(svg(LIGHT))
open(f"{target}/werkrevisor-review-decisions-dark.svg", "w").write(svg(DARK))