FR-049.1 問卷顯示條件(目標側重構)— Design

母案:FR-049(來源側 goto 跳題,四段已完成 commit 未部署)。本案是 user 2026-07-18 手測後拍板的資料模型翻轉:跳題規則從「來源選項宣告目的地」改為「目標題目/題組自己宣告顯示條件」——業界主流(Qualtrics/SurveyMonkey)形態。 狀態:design 拍板(六決策見 §2),implementation plan 待寫。 前提:FR-049 未部署 STG、無生產資料用 goto_* 新鍵、E2E 未 commit——現在翻轉零遷移成本,晚一版成本翻倍。

1. 為什麼推翻 FR-049 的來源側模型

user 手測 Designer 時三連問(「這兩個是不是擇一」「為什麼可複選」「BC 都要打幹嘛設跳轉」)暴露的不是文案問題,是模型錯位:

來源側瑕疵 說明
隱式副作用 題組 B 變條件題組,是「被別題的選項提到」的副作用,B 自己身上無跡可循
漏網坑 來源題新增選項忘設規則 → 答該選項者整章看不到,Designer 零警告(靜默漏答)
零全局視圖 規則散在各題 dialog 深處,無法一眼看出誰是條件題組、被誰控制
心智模型錯位 「跳轉至題組(可複選)」——「混合跳去兩個地方」語意荒謬;真語意是「此答案適用哪些章節」

目標側一次解決:條件寫在目標身上(自描述)、條件不成立即不顯示(無覆蓋問題)、列表卡片可掛條件 badge(全局視圖免費)、「我何時出現」是使用者真正在想的問題。

需求形狀確認:user 拍板本產品問卷是「通用為主、例外分流」(多數章節人人答,少數依情況出現)——目標側模型直接命中,不需要「完全分支」的批次隱藏工具(列 backlog)。

2. 決策紀錄(user 2026-07-18 全數拍板)

# 決策 結論
1 跳題模型 A 案:目標側顯示條件,來源側 goto UI 整個拿掉
2 子題語意 純排版附屬(編號/縮排/一起出現一起答);「答了才展開」= 對子題設顯示條件的機制在 BE/引擎層保留,但 v1 不給 UI 入口(決策 9 二修:僅 FE 隱藏)
3 子題建立入口 母題卡片直接「+ 新增延伸題」,dialog 的「父題目」下拉拿掉
4 同組跳題操作殼 (a) 只做目標側 UI2026-07-18 段② 手測後改判 (b):跳題精靈提前入本案(決策 7)
5 FR-049 已 commit 四段 不 revert,FR-049.1 以 delta 疊上(保留/重做清單見 §5)
6 E2E 舊模型場景止損,FR-049.1 定案後重派
7 跳題精靈(段② 手測追加) user 實際手測體感「要跑到目標題身上設定、還要回頭找來源題選項」摩擦過大——來源題編輯 dialog 加「依此題答案控制顯示」區:選目標(題目/題組,可多目標) + 圈答案 → 存檔時自動翻譯成目標側 display_condition 寫到各目標身上。資料模型不動(仍只有目標側 display_condition,精靈是純 UI 翻譯層);目標側「顯示條件」區與 badge 照舊保留,兩入口殊途同歸、互相同步顯示
8 來源題型擴充(段② 手測追加) dropdown(下拉選擇)納入合法來源題型——語意同為單選。BE 驗證(SOURCE_NOT_RADIO 五擋之一)與 FE 來源題過濾同步放寬為 type ∈ {radio, dropdown};error code 文案改「來源題必須是單選類型(單選題/下拉選擇)」
9 子題跳題「僅 FE 隱藏」(段② 手測追加,2026-07-18 二修) 延伸題(子題)的顯示條件 UI 前端不顯示——編輯子題的 dialog 隱藏「顯示條件」區與跳題精靈區、精靈目標下拉排除子題。BE 能力保留不加擋(display_condition 落子題仍合法、引擎照算)——日後要開放「答了才展開」只需把 FE 顯示打開,零 BE 改動。決策 2 的「對子題設條件=答了才展開」語意在 BE/引擎層仍成立,只是 v1 不給 UI 入口。子題不作 source 維持不變(既有排除)
10 子題題號自動衍生(段② 手測追加) 「+新增延伸題」建立時題號自動帶「母題號-序」:母 AC-1 → 子依序 AC-1-1、AC-1-2(取母題現有子題數+1);欄位仍可手改。刪中間子題不回填重排(避免題號漂移)
11 Badge 全文化 + 來源側反向提示(段② 填答手測追加) (A) 目標側 badge 改完整條件語句「當 AC-2=『已全面實施』或『部分實施』時顯示」,答案多時截斷+tooltip 全文;(B) Designer 編輯來源題時,每個選項列尾端顯示反向提示「→ 顯示:AC-2-1、B 題組」/「(無影響)」(資料=精靈反向索引,純渲染)
12 填答端選項提示(段② 填答手測追加) 會展開追加題的選項尾端加輕量提示「+將增加 N 題」——只顯示題數不顯示內容(避免答題誘導偏誤,對齊 Qualtrics 受測端預設)
13 雙入口統一語言(段② 填答手測追加,選項 a) 維持目標側+來源側精靈雙入口,以視覺語言表明同一份資料:①兩區標題同詞根(目標側「顯示條件——此題何時顯示」/來源側「顯示條件——此題答案控制誰顯示」)②精靈區頂部說明「在此設定的規則會寫入各目標題的顯示條件,兩處是同一份設定」③配合決策 11 的全局視圖。不砍任一入口(使用者兩種心智模型並存)
14 條件答案欄反面提示(snapshot 手測追加) user 設條件後填答才發現「未圈中的答案作答=隱藏」——三處條件答案 MultiSelect(目標側/精靈/題組)下方加即時反面提示「未勾選的答案({names})作答時,本題/本題組將隱藏」,把「未命中=隱藏」的後果攤在設定當下。answers 空/未選來源/全圈不顯示
15 決策 12 修訂:題數提示改靜態計數(snapshot 手測追加) what-if 動態 diff 在「未作答=顯示」語意下恆 0(受控題本來就顯示)——改靜態反向索引:N=引用「本題+本選項」的目標題數(page 目標計其下全部題目含子題),與顯示狀態無關、空白問卷即顯示

3. 資料模型

3.1 顯示條件(新,掛在目標上)

SurveyQuestion.options 之外,題目與題組本體各加一個 JSONB 鍵 display_condition:

// SurveyQuestion(題目)與 SurveyPage(題組)同形狀
"display_condition": {
  "source_question_uid": "<單選題 uid>",     // 條件來源(v1 範圍:單一來源、單選題)
  "answers": ["<option key>", ...]           // 來源題答這些值時本題/題組顯示
} | null                                      // null / 缺省 = 無條件恆顯示
  • 語意:display_condition 存在 → 僅當來源題已作答且答案 ∈ answers 時顯示;來源題未作答時顯示(維持 FR-049「答了才略過」語意,保證空白問卷全可見)——注意這與「答案 ∈ answers」判斷相互獨立:未作答=顯示,已作答才比對集合
  • answers 比對鍵:沿 FR-049 段④ 的教訓,用 option 的 key || name(FE mapOptionFromApi 同款取值)。
  • v1 範圍限制(明確不做):多來源條件(AND/OR 跨題)、非單選來源、跨問卷——BE 驗證直接擋。
  • goto_ 鍵退役*:goto_question_uid/goto_page_uids 不讀不寫;FR-049 從未部署,無資料要遷移。jedi-survey 段① 寫的 goto 驗證邏輯同步退役。

3.2 儲存位置

  • 題目:SurveyQuestion.options 同表另一 JSONB 欄?——display_condition 是題目級屬性非選項級,落 SurveyQuestion 本體(實作時查 jedi-survey model 有無現成 JSONB 雜項欄可掛,無則 options 平級新增欄位——落點在 implementation plan 前提查證定案,含可翻譯欄位家族檢查:若落在 translatable_fields 內的欄位,必須複查 jedi-common 翻譯 bug 耦合,見 docs/issues/pending/2026-07-18-jedi-common-translation-exclude-never-works.md)。
  • 題組:SurveyPage 本體同款。

3.3 稽核快照(沿用 FR-049 段④ 機制,換內容)

survey.question_answer_histories.flow_rules_snapshot(DEV 已套的欄位照用,不回滾)——內容從 goto 鍵集合改為「當下全部 display_condition 的快照」:

[{ "target_type": "question|page", "target_uid": "...",
   "source_question_uid": "...", "answers": [...] }, ...]

checkpoint 落快照、revert 繼承目標快照、null 容忍——三個行為與 FR-049 裁決完全一致,只換抽取來源。

4. UI 行為

4.1 Designer

  • 題目/題組設定各加「顯示條件」區:「當 [選一個單選題▼] 的答案是 [選項多選▼] 時顯示」+ 清除鈕。來源題下拉只列同問卷單選題(排除自己與自己的子題)。
  • AddQuestionDialog 拿掉:「父題目」下拉(決策 3)、「流程規則」整區(goto UI 退役)。「進階設定」步驟只剩補充描述——步驟可視內容合併,實作時定。
  • 列表卡片掛條件 badge:有 display_condition 的題目/題組顯示「條件顯示:依 <來源題編號>」badge——全局視圖。
  • 子題:母題卡片 footer 加「+ 新增延伸題」;子題卡片縮排嵌套在母題卡片內(修 FR-049 遺留:目前列表模式 1-1-1 仍是平級獨立卡片,user 截圖實錘)、子題不提供獨立上移/下移(跟著母題走)。
  • 來源題防呆:刪除被引用的來源題 → 連動清空引用它的 display_condition(沿段① 清理 hook 模式,對象換掉);來源題從單選改為其他題型 → 同樣清空並提示。

4.2 填答端(行為不變,輸入換形狀)

computeVisibility 純函數保留,輸入從「掃 options goto 鍵」改為「掃全部 display_condition」;渲染/進度/必填三消費點同源架構原封不動。已填後被隱藏的答案保留不刪(稽核軌跡)——全部沿 FR-049。

4.3 稽核檢視端(行為不變)

「不適用 vs 漏答」判定沿 FR-049 段④:讀快照重算,灰標「不適用(依 XX 作答略過)」/紅標必填。computeVisibilityFromSnapshot 換輸入形狀。

5. FR-049 資產處置(決策 5:delta 不 revert)

資產 處置
可見性引擎骨架(useSurveyVisibility.js 雙函數) 保留,換輸入解析
快照機制(migration/model→serializer 全鏈/checkpoint/revert 繼承) 保留,換抽取內容
三消費點同源架構(SurveyPreview 重構) 保留
子題 pid 接線(mapper/useSurveyApi/BE) 保留
檔案上傳 dropzone 等樣式修 保留(與模型無關)
jedi-survey 段① goto 驗證(_validate_flow_rules)+ 14 測試 重做:改驗 display_condition(單選來源/禁自指/目標存在/同問卷——邏輯同款換對象);goto 驗證與測試退役
jedi-survey 段① 清理 hook 改造:清理對象從「選項 goto 鍵」改為「引用該題的 display_condition」
Designer 流程規則 UI(AddQuestionDialog 三行卡片) 退役,換目標側「顯示條件」區
survey-mapper goto 鍵映射 重做:改映 display_condition
E2E 舊模型場景 已止損;子題/既有回歸類經新設計比對後沿用,跳題類重寫

6. 驗收情境

  1. 部署環境三分流(通用+例外形狀):12 題組,僅 B 設「A-1∈{地端,混合}」、C 設「A-1∈{雲端,混合}」→ 答地端:A,B,D~L;答雲端:A,C,D~L;答混合:全部;未作答:全部可見。
  2. 新增選項無漏網:A-1 加新選項「其他」不動任何條件 → 答「其他」:B、C 不顯示(條件字面語意),D~L 照常——無靜默漏答整章的坑。
  3. 同組跳題:Q2-Q4 各設「Q1≠2」→ Q1 答 2 時 Q2-Q4 消失、進度分母同步;改答恢復、答案保留。
  4. 必填同源:被條件隱藏的必填題不擋提交;可見必填未填仍擋。
  5. 稽核回溯:提交後 Designer 改條件 → 稽核檢視按提交當下快照判定「不適用」,不漂移;revert 版本繼承目標快照。
  6. Designer 防呆:刪來源題/來源題改題型 → 引用它的條件被連動清空;條件 badge 正確顯示與消失。
  7. 子題:母題卡「+ 新增延伸題」建立 → 子題嵌套顯示於母題卡內、無獨立排序;填答端跟母題一起出現。

7. Backlog(明確不在本案)

  • 跳題精靈 → 2026-07-18 決策 7 提前入本案(見 §2)
  • 多來源/複合條件(AND/OR)
  • 「完全分支」批次隱藏工具
  • 非單選類來源題型(checkbox/text 等;dropdown 已由決策 8 納入)
  • 子題「答了才展開」的 UI 入口(BE/引擎能力已在,決策 9 二修僅 FE 隱藏;日後開放=打開 FE 顯示即可)