ここまで3つの課題を紹介してきました。今回は、その3つを共通で貫いている仕組みの話です。手順を並べるだけでは足りなかった「完了条件」を、どう標準化したかを書きます。
チェックリストは、形だけになりやすい
確認観点を並べると、見た目はチェックリストに近づきます。ただ、チェックリストには弱点があります。中身を見ずに、とりあえずチェックを付けてしまえる、ということです。
手順を表示するだけでは、品質はそろいません。表示された項目を本当に確認したのか、証跡を残したのか、正常か異常かを判断したのか。ここが自己申告のままだと、第2回で書いた「操作はできたが判断はしていない」状態を、結局は止められません。
そこで、表示するだけでなく、条件を満たすまで先へ進めない、という制約を入れることにしました。
完了に足りないものを、具体的に示す
各ステップには、必須の証跡と、正常か異常かの記録が定義されています。これらが埋まっているかを判定するのが、次の純粋関数です。
export function stepMissing(step: CaseStep, sp: StepProgress): string[] {
const missing: string[] = []
for (const req of step.evidenceRequirements) {
if (req.required && !(sp.evidence[req.id]?.trim())) {
missing.push(`証跡「${req.label}」を入力してください`)
}
}
if (step.requiresJudgement && sp.judgement === null) {
missing.push('正常/異常の判断を記録してください')
}
return missing
}
不足している項目を、メッセージの配列として返します。空でなければ、まだ完了できません。ポイントは、ただ「できません」と止めるのではなく、何が足りないかを具体的に示すことです。画面の中央に「証跡◯◯を入力してください」と並ぶので、次に何をすればいいかが分かります。
未入力では、次へ進めない
この判定を、そのまま「次へ」の制約に使っています。現在のステップが完了していなければ、次へは無効です。
export function canAdvance(caseGuide, progress, stepId): boolean {
const step = getStep(caseGuide, stepId)
if (!step) return false
return isStepDone(step, progress.steps[stepId] ?? emptyStepProgress())
}
isStepDone は、完了フラグが立っていて、かつ先ほどの不足チェックを通っていることを見ています。証跡を残し、判断を記録して、初めて次へ進める。手順の表示ではなく、完了の条件そのものを仕組みで守る、という形です。
課題全体の完了も同じ考え方です。すべてのステップが完了していて、なおかつ最後の最終確認にチェックが付いたときに、初めて課題完了になります。未完了のステップが残っていれば、その名前を表示します。
「対象外」「未確認」も、正しく扱う
前回の環境調査の回で書いたとおり、判断は正常・異常だけではありません。存在しない要素には「該当なし(対象外)」があり、まだ分からないものには「未確認」があります。
完了制約を作るとき、ここを取り違えないよう気をつけました。「対象外」は、確認したうえで無いと分かった状態なので、判断として記録すれば完了できます。一方で空欄は、まだ何も判断していない状態なので、完了できません。分からないなら「未確認(確認先・期限・担当)」と書く。この区別を、制約の側でも壊さないようにしています。
進捗はlocalStorageに保存する
もう一つ大事なのが、進捗が消えないことです。長い課題の途中でブラウザを閉じたら最初から、では使えません。
そこで、現在のステップ、各ステップの完了状態、証跡の入力、正常・異常の判断、最終確認を、ブラウザのlocalStorageに保存しています。キーは opsmate-case-progress-v1:{caseId} という形で、課題ごとに分けています。変更のたびに保存し、再読み込みで復元されます。
この進捗は、アプリの他のデータ(IndexedDBに入っているもの)とは別系統にしてあります。課題ガイドの進捗だけを、軽く独立して持たせる、という割り切りです。読み込み時には、保存済みの進捗を今の課題定義と突き合わせて整合させる処理を通すので、後からステップが増えても壊れにくくしています。
制約は、厳しくしすぎてもいけない
作りながら難しかったのは、制約の強さの加減です。厳しくするほど品質はそろいますが、厳しすぎると、今度は「本当は対象外なのに入力させられる」ような窮屈さが出ます。
だから、必須にするのは各ステップで最低限の証跡と判断にとどめ、無い項目は「対象外」で通せるようにしました。抜けは止めたいけれど、正直に「無い」「未確認」と書ける道は残す。このバランスは、今も正解が出ているとは言えず、課題データを書きながら調整している部分です。
作ってみて感じたこと
「押せない」という制約は、地味ですが効きます。表示するだけのときと比べて、証跡と判断が自然に残るようになりました。手順だけでなく完了条件まで標準化する、という当初の狙いには、この仕組みで一歩近づけたと思います。
一方で、制約はデータの質に依存します。完了条件の設定が甘ければ、押せてしまう抜けも残ります。仕組みは器で、中身は課題ごとに詰めるしかない、という構図はここでも同じでした。
次回は、この共通の仕組みと関係の深い、AIの役割の話です。なぜAIに全部を答えさせず、確認観点だけを補助させるのか。ハルシネーションとAPIの費用の観点から書きます。