先に二つ明かす。
第一に、2026年4月にこの雑誌は、ある医師が診療フローの道具を作った話を載せた。この現場は特定の医院ではない。 実際にそう回っている形をそのまま構成し、どこかは明かさない。以下はその形どおりに自分で作ったものであり、手順と数値は作り物だ。
第二に、この記事は医療助言ではなく、ここで作ったものも医療機器ではない。 このシリーズは臨床検証をしていないし、する位置にもいない。扱うのはソフトウェアの境界ひとつ — 判断を道具の外に置くことがコードでどう見えるか。
言葉で引く線は消える
元の記事にもその線はあった。「診断は人に残し、診療の流れだけを画面に整理する。」
正しい言葉だ。問題はそれが文としてだけあったことだ。
文としてある線は消える。特にこういう消え方をする — 数か月後に誰かが「要約が一行あると便利なんだけど」とフィールドをひとつ足す。そのフィールドに おおむね正常範囲 のような文字列が入る。誰もその瞬間に気づかない。 道具が判定を始めた瞬間だ。
だから今回はコードに引いた。二度。
第一の線 — そんな道具がない
tools offered: flow.state, flow.done, readings.list
三つだけだ。手順を見せ、手順ひとつを完了に印を付け、計測値をそのまま返す。判定・分類・解釈をする道具がない。
検証が道具の名前から見る。
for W in diagnos assess triage interpret advise; do
if grep -qi "tools offered:.*$W" captures/run.log; then
echo " a tool is named like it produces a judgement: $W"; exit 1
fi
done
これは先行する編で確認した原理の適用だ。道具一覧がすなわち表面であり、一覧にない能力は人が押してもモデルが選んでも届かない。
第二の線 — 判定のように聞こえれば例外が出る
一覧だけでは足りない。flow.state が返す文字列の中に判定が混じりうるからだ。
/// Words that would turn a report into a verdict. Anything this server is
/// about to say is checked against them.
///
/// A list of words is a crude guard and it is meant to be. It cannot stop a
/// determined author, but it does stop the ordinary way this line gets
/// crossed: someone adds a helpful-sounding summary field months later and
/// nobody notices that the tool started diagnosing.
static const forbidden = [
'diagnos', 'likely', 'suggests', 'consistent with', 'probable',
'abnormal', 'normal', 'healthy', 'concerning', 'severe', 'mild',
'recommend', 'should take', 'prescribe',
];
static String _guard(String s) {
final lower = s.toLowerCase();
for (final w in forbidden) {
if (lower.contains(w)) {
throw StateError('clinic_server tried to emit a judgement word: "$w"');
}
}
return s;
}
サーバーが出すすべての文字列がこの関数を通る。 normal が一覧にあるのを見てほしい — 最も無害に見える語であり、最もよく線を越える語だ。
粗いガードだ。本気の人は止められない。ところがこの線が実際に消える仕方は本気ではなく不注意であり、不注意はこの程度でも止まる。
そして検証が実行全体を走査する。
no judgement vocabulary in anything the server returned