commit 2066ac279221c036615d73a0283556d1e053e338
Author: Eiichi Shimazu <studio@adaptdesign.jp>
Date:   Sat Nov 14 23:24:36 2026 +0900

    判定の画面の pick / place を日本語にし place に「そこまで行かなかった」を足す (依頼 193). index.html: ボタンの字を つかむ (つかめた・つかめなかった・分からない) / 置く (置けた・置けなかった・そこまで行かなかった・分からない) に. 値は yes / no / unknown のまま, place だけ not_applicable を足す. AI の判定・今の判定・この回の行・知らせも同じ日本語. つかめなかったで置くは自動でそこまで行かなかった (選び直せる), つかめたに戻すと置くは選んでいない状態に. 選ばないときの値は今までどおり (説明の字だけ日本語). verdicts.py: PLACE_VALUES を足し place だけ not_applicable を通す. test_verdicts.py に 4 件. テスト 49 OK. 8765 番は /tmp の写しで撮影ののち本物の判定ファイルで立ち上げ直した (本物は md5 変わらず)
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---
 pi05/verdict_viewer/index.html       | 28 +++++++++++++--------
 pi05/verdict_viewer/test_verdicts.py | 49 ++++++++++++++++++++++++++++++++++++
 pi05/verdict_viewer/verdicts.py      |  7 ++++--
 3 files changed, 72 insertions(+), 12 deletions(-)

diff --git a/pi05/verdict_viewer/index.html b/pi05/verdict_viewer/index.html
index 1d95122..a9ca0ba 100644
--- a/pi05/verdict_viewer/index.html
+++ b/pi05/verdict_viewer/index.html
@@ -141,11 +141,11 @@
     <button class="key" data-oc="unknown"><kbd>u</kbd> 分からない</button>
     <button class="key" data-oc="not_judgeable"><kbd>x</kbd> 判定できない</button>
   </div>
-  <div class="row"><span class="lbl">pick</span>
-    <div class="seg"><button data-pp="pick" data-v="yes">yes</button><button data-pp="pick" data-v="no">no</button><button data-pp="pick" data-v="unknown">unknown</button></div></div>
-  <div class="row"><span class="lbl">place</span>
-    <div class="seg"><button data-pp="place" data-v="yes">yes</button><button data-pp="place" data-v="no">no</button><button data-pp="place" data-v="unknown">unknown</button></div></div>
-  <p class="note">選ばなければ 成功→yes / ほか→unknown</p>
+  <div class="row"><span class="lbl">つかむ</span>
+    <div class="seg"><button data-pp="pick" data-v="yes">つかめた</button><button data-pp="pick" data-v="no">つかめなかった</button><button data-pp="pick" data-v="unknown">分からない</button></div></div>
+  <div class="row"><span class="lbl">置く</span>
+    <div class="seg"><button data-pp="place" data-v="yes">置けた</button><button data-pp="place" data-v="no">置けなかった</button><button data-pp="place" data-v="not_applicable">そこまで行かなかった</button><button data-pp="place" data-v="unknown">分からない</button></div></div>
+  <p class="note">選ばなければ 成功→つかめた・置けた / ほか→分からない</p>
   <button id="confirm" disabled>確定<kbd>Enter</kbd></button>
   <h2>いま確定した回 (新しい 5 回)</h2><div id="recent"></div>
   <h2>ひとこと</h2>
@@ -160,6 +160,10 @@
 </aside>
 <script>
 const OC_JA = {success: "成功", failure: "失敗", unknown: "分からない", not_judgeable: "判定できない", not_applicable: "試行でない", "": "なし"};
+// pick / place の値は yes / no / unknown (place だけ not_applicable も) のまま. 画面の字だけ日本語 (依頼 193)
+const PICK_JA = {yes: "つかめた", no: "つかめなかった", unknown: "分からない"};
+const PLACE_JA = {yes: "置けた", no: "置けなかった", not_applicable: "そこまで行かなかった", unknown: "分からない"};
+const ppJa = r => `つかむ ${esc(PICK_JA[r.pick] ?? r.pick)} / 置く ${esc(PLACE_JA[r.place] ?? r.place)}`;
 const LB_JA = {run_input: "記録時の入力", ai: "AI", human: "人 (AI と同じ)", human_fixed_ai: "人 (AI を直した)", human_only: "人 (AI なし)", rule: "決め (依頼185)", "": ""};
 const COLORS = ["--s1", "--s2", "--s3", "--s4", "--s5", "--s6"].map(v => getComputedStyle(document.documentElement).getPropertyValue(v).trim());
 const pendingOps = {};  // run_stem → 書けたか分からない操作 (回ごとに残す. codex 6 回目 R1)
@@ -282,12 +286,12 @@ function stepFrame(d) { if (detail && !detail.has_mp4 && detail.frames.length &&
 
 function renderInfo() {
   const a = detail.ai, l = detail.latest;
-  $("#ai").innerHTML = a ? `<div class="big">${OC_JA[a.outcome]}</div><div>pick ${a.pick} / place ${a.place} ・ 素材 ${a.evidence || "なし"}</div><div class="muted">${esc(a.ai_reason)}</div>` : `<span class="muted">AI の判定はありません</span>`;
+  $("#ai").innerHTML = a ? `<div class="big">${OC_JA[a.outcome]}</div><div>${ppJa(a)} ・ 素材 ${a.evidence || "なし"}</div><div class="muted">${esc(a.ai_reason)}</div>` : `<span class="muted">AI の判定はありません</span>`;
   const h = detail.history.filter(r => HUMAN.includes(r.labeler)).pop();  // 人が確定した行 (依頼 187)
   const c = h || l;  // 札は 1 回だけ. 札・人が確定・時刻・pick / place を 1 行目に (依頼 190)
-  $("#cur").innerHTML = c ? `<div>${tag(c.outcome, c.labeler === "ai")} ${h ? "<b>人が確定済み</b>" : `<span class="muted">${LB_JA[c.labeler]}</span>`} ・ ${esc(c.labeled_at.slice(0, 10))} ${hm(c.labeled_at)} ・ pick ${c.pick} / place ${c.place}</div>${c.note ? `<div class="muted">${esc(c.note)}</div>` : ""}` : `<span class="muted">まだ判定がありません</span>`;
+  $("#cur").innerHTML = c ? `<div>${tag(c.outcome, c.labeler === "ai")} ${h ? "<b>人が確定済み</b>" : `<span class="muted">${LB_JA[c.labeler]}</span>`} ・ ${esc(c.labeled_at.slice(0, 10))} ${hm(c.labeled_at)} ・ ${ppJa(c)}</div>${c.note ? `<div class="muted">${esc(c.note)}</div>` : ""}` : `<span class="muted">まだ判定がありません</span>`;
   markOC();
-  $("#hist").innerHTML = detail.history.length ? `<table class="hist"><tr><th>時刻</th><th>判定</th><th>pick/place</th><th>付けた</th></tr>${detail.history.map(r => `<tr><td>${esc(r.labeled_at)}</td><td>${tag(r.outcome, r.labeler === "ai")}</td><td>${r.pick}/${r.place}</td><td>${LB_JA[r.labeler]}</td></tr>`).join("")}</table>` : `<span class="muted">なし</span>`;
+  $("#hist").innerHTML = detail.history.length ? `<table class="hist"><tr><th>時刻</th><th>判定</th><th>つかむ/置く</th><th>付けた</th></tr>${detail.history.map(r => `<tr><td>${esc(r.labeled_at)}</td><td>${tag(r.outcome, r.labeler === "ai")}</td><td>${esc(PICK_JA[r.pick] ?? r.pick)}/${esc(PLACE_JA[r.place] ?? r.place)}</td><td>${LB_JA[r.labeler]}</td></tr>`).join("")}</table>` : `<span class="muted">なし</span>`;
 }
 
 // ---- 関節と爪のグラフ (関節は 1 本の軸・爪は下に別の帯. 2 つの縦軸は使わない) ----
@@ -365,7 +369,11 @@ function markPP() {
   document.querySelectorAll("[data-pp]").forEach(b => b.classList.toggle("on", pp[b.dataset.pp] === b.dataset.v));
 }
 document.querySelectorAll("[data-pp]").forEach(b => b.onclick = () => {
-  const k = b.dataset.pp; pp[k] = pp[k] === b.dataset.v ? null : b.dataset.v; markPP(); b.blur();
+  const k = b.dataset.pp, prev = pp[k]; pp[k] = prev === b.dataset.v ? null : b.dataset.v;
+  // つかめなかった → 置くは自動でそこまで行かなかった (選び直せる). つかめなかったからつかめたに戻したら置くは選んでいない状態に (依頼 193)
+  if (k === "pick" && pp.pick === "no") pp.place = "not_applicable";
+  else if (k === "pick" && pp.pick === "yes" && prev === "no") pp.place = null;
+  markPP(); b.blur();
 });
 document.querySelectorAll("[data-oc]").forEach(b => b.onclick = () => { b.blur(); choose(b.dataset.oc); });
 $("#confirm").onclick = () => { $("#confirm").blur(); confirmChosen(); };
@@ -398,7 +406,7 @@ async function save(outcome) {
     if (!res.ok) { delete pendingOps[stem]; markOC(); status("書けませんでした: " + j.error, "err"); return; }
     saved = true;
     toast(`確定しました：${esc(stem)}${tag(outcome, false)}`);
-    status(`${stem} を「${OC_JA[outcome]}」で追記しました (${LB_JA[j.row.labeler]}・pick ${j.row.pick} / place ${j.row.place})`, "ok");
+    status(`${stem} を「${OC_JA[outcome]}」で追記しました (${LB_JA[j.row.labeler]}・${ppJa(j.row)})`, "ok");
     await loadRuns();
     // 同じ位置 (確定で一覧から消えたらそこに来た次の回) へ進む
     let next;
diff --git a/pi05/verdict_viewer/test_verdicts.py b/pi05/verdict_viewer/test_verdicts.py
index 106c984..7ab46a9 100644
--- a/pi05/verdict_viewer/test_verdicts.py
+++ b/pi05/verdict_viewer/test_verdicts.py
@@ -86,6 +86,15 @@ class TestAppend(unittest.TestCase):
                 V.append_rows([row("pi05_9", "success", "ai"), bad], self.csv)
         self.assertEqual(self.csv.read_bytes(), before, "まちがった行のときに途中まで書いた")
 
+    def test_place_not_applicable_only_for_place(self):
+        # 依頼 193: place は not_applicable (そこまで行かなかった) を書ける. pick には書けない
+        V.append_rows([row("pi05_1", "failure", "human_only", pick="no", place="not_applicable")], self.csv)
+        self.assertEqual(V.read_rows(self.csv)[-1]["place"], "not_applicable")
+        before = self.csv.read_bytes()
+        with self.assertRaises(ValueError):
+            V.append_rows([row("pi05_2", "failure", "human_only", pick="not_applicable", place="not_applicable")], self.csv)
+        self.assertEqual(self.csv.read_bytes(), before)
+
     def test_comma_and_newline_in_reason(self):
         V.append_rows([row("pi05_1", "failure", "ai", ai_reason='箱の外, "落とした"\n2 行目')], self.csv)
         self.assertEqual(V.read_rows(self.csv)[0]["ai_reason"], '箱の外, "落とした"\n2 行目')
@@ -342,6 +351,16 @@ class TestServer(unittest.TestCase):
         self.assertTrue(runs["pi05_20261001_000003"]["confirmed"])
         self.assertEqual(runs["pi05_20261001_000003"]["outcome"], "success")
 
+    def test_post_place_not_applicable(self):
+        # 依頼 193: 画面から place=not_applicable を送れる. pick=not_applicable は 400 で書かない
+        st, j = self.post({"run_stem": "pi05_20261001_000003", "outcome": "failure", "pick": "no", "place": "not_applicable"})
+        self.assertEqual(st, 200)
+        self.assertEqual((j["row"]["pick"], j["row"]["place"]), ("no", "not_applicable"))
+        before = self.csv.read_bytes()
+        st, j = self.post({"run_stem": "pi05_20261001_000003", "outcome": "failure", "pick": "not_applicable"})
+        self.assertEqual(st, 400)
+        self.assertEqual(self.csv.read_bytes(), before)
+
     def test_same_op_id_writes_once(self):
         n = len(V.read_rows(self.csv))
         b = {"op_id": "abc", "run_stem": "pi05_20261001_000003", "outcome": "failure"}
@@ -493,6 +512,36 @@ class TestConfirm192(unittest.TestCase):
         self.pg.wait_for_timeout(600)
         self.assertEqual(self.n_rows(), n)
 
+    # ---- 依頼 193: つかむ / 置くを日本語に・つかめなかったで置くは自動でそこまで行かなかった ----
+    def pp_on(self):
+        return self.pg.eval_on_selector_all("[data-pp].on", "e => e.map(x => x.dataset.pp + '=' + x.dataset.v)")
+
+    def test_pp_labels_are_japanese(self):
+        got = self.pg.eval_on_selector_all("[data-pp]", "e => e.map(x => x.dataset.pp + '=' + x.dataset.v + ':' + x.textContent)")
+        self.assertEqual(got, ["pick=yes:つかめた", "pick=no:つかめなかった", "pick=unknown:分からない",
+                               "place=yes:置けた", "place=no:置けなかった", "place=not_applicable:そこまで行かなかった",
+                               "place=unknown:分からない"])
+        self.assertEqual(self.pg.eval_on_selector_all("#panel .row .lbl", "e => e.map(x => x.textContent)"), ["つかむ", "置く"])
+
+    def test_pick_no_selects_place_not_applicable(self):
+        n = self.n_rows()
+        self.pg.click('[data-pp="pick"][data-v="no"]')
+        self.assertEqual(self.pp_on(), ["pick=no", "place=not_applicable"])
+        self.pg.click('[data-pp="place"][data-v="unknown"]')  # 選び直せる
+        self.assertEqual(self.pp_on(), ["pick=no", "place=unknown"])
+        self.pg.click('[data-pp="pick"][data-v="yes"]')  # つかめたに戻すと置くは選んでいない
+        self.assertEqual(self.pp_on(), ["pick=yes"])
+        self.pg.click('[data-pp="pick"][data-v="no"]')
+        self.pg.wait_for_timeout(300)
+        self.assertEqual(self.n_rows(), n)  # 選ぶだけでは書かない
+        stem = self.pg.text_content("#title")
+        self.pg.keyboard.press("n")
+        self.pg.keyboard.press("Enter")
+        self.pg.wait_for_timeout(1000)
+        rows = V.read_rows(self.csv)
+        self.assertEqual(len(rows), n + 1)
+        self.assertEqual((rows[-1]["run_stem"], rows[-1]["pick"], rows[-1]["place"]), (stem, "no", "not_applicable"))
+
 
 class TestRule185(unittest.TestCase):
     """エイイチの決め (2026-11-14 依頼 185): run_input も確定・送ったチャンク 0 → not_applicable・素材不足 → not_judgeable."""
diff --git a/pi05/verdict_viewer/verdicts.py b/pi05/verdict_viewer/verdicts.py
index e88a95e..ed520f3 100644
--- a/pi05/verdict_viewer/verdicts.py
+++ b/pi05/verdict_viewer/verdicts.py
@@ -27,6 +27,7 @@ VERDICTS_CSV = DATA_ROOT / "runs" / "pi05_policy_verdicts_v1.csv"
 COLUMNS = ["labeled_at", "run_stem", "pick", "place", "outcome", "labeler",
            "ai_outcome", "ai_reason", "evidence", "note"]
 PICK_PLACE = ("yes", "no", "unknown")
+PLACE_VALUES = PICK_PLACE + ("not_applicable",)  # つかめず place まで行かなかった回 (依頼 193. pick には足さない)
 OUTCOMES = ("success", "failure", "unknown", "not_judgeable", "not_applicable")
 AI_OUTCOMES = ("success", "failure", "unknown", "not_judgeable")  # AI が付けてよい outcome (not_applicable は rule だけ)
 LABELERS = ("run_input", "ai", "human", "human_fixed_ai", "human_only", "rule")
@@ -65,8 +66,10 @@ def validate_row(row):
             r[c] = r[c][:n] + "…"
     if not r["run_stem"]:
         raise ValueError("run_stem が空です")
-    if r["pick"] not in PICK_PLACE or r["place"] not in PICK_PLACE:
-        raise ValueError(f"pick / place は {PICK_PLACE} のどれか: {r['pick']!r} {r['place']!r}")
+    if r["pick"] not in PICK_PLACE:
+        raise ValueError(f"pick は {PICK_PLACE} のどれか: {r['pick']!r}")
+    if r["place"] not in PLACE_VALUES:
+        raise ValueError(f"place は {PLACE_VALUES} のどれか: {r['place']!r}")
     if r["outcome"] not in OUTCOMES:
         raise ValueError(f"outcome は {OUTCOMES} のどれか: {r['outcome']!r}")
     if r["labeler"] not in LABELERS:
