Skip to content

feat: add week view time-axis mode - #163

Merged
cheerc merged 11 commits into
mainfrom
feat/162-week-view-time-axis
Jul 20, 2026
Merged

feat: add week view time-axis mode#163
cheerc merged 11 commits into
mainfrom
feat/162-week-view-time-axis

Conversation

@cheerc

@cheerc cheerc commented Jul 20, 2026

Copy link
Copy Markdown
Owner

What

新增週檢視「依時間」時間軸模式,支援七日欄位、課程卡老師+教室、持久化還原、全部課表唯讀、week-time PDF,以及空白格安全 guard。日檢視行為維持不變。

How

  • 選擇分支 A:新增 ViewDecisionHelpers.js.html,由 Index.html 在 UI/JavaScript 前載入,供 restore、renderer dispatch、card fields、PDF decisions 使用。
  • UI.js.html 以 uiModule receiver 的 this._flattenCoursesForDays / this._groupCoursesByStartTime 收斂四個 renderer;week-time renderer 依 timeStart 動態分組並保留 classroom/day dataset。
  • JavaScript.html 採 view-scoped lastViewSortMode:day 強制 time,week 合法值還原,非法值 fallback classroom
  • PDFExport.js.html 對 week-time 輸出老師+教室,對角標籤只在 week-time 顯示「時間」。

Scope

follows plan and closes issue #162. Changed files are limited to the planned UI, PDF, interaction guard, extracted decision module, tests/lib mirrors/contracts, manual TestCases, and independent verification checklist. DataCollection.js.html upcoming producer 與 JavaScript.html timer 未修改。

Verification at head 0b1fd82c197d14601133ec93bb2dd1301c89fe98:

  • ./workflow.sh → t1, t2, t3, t4, t5, t7, t8, t9 全部 PASS(8/8)
  • Vitest → 37 files, 1187 tests passed
  • PHPUnit → 60 tests, 117 assertions passed
  • ESLint → 0 errors(既有 warnings)

Production HTML 的 WT-1~WT-9 尚待 clasp push dev/preview 後依 provenance 規則執行;獨立 checklist:docs/verification-checklist-2026-07-20-week-view-time-axis.md

Lessons

  • dispatch-bound worktree 的 .agend-managed marker 會讓 alignment script 將唯一 untracked marker 誤判為 dirty;依 lead 授權保留 marker,使用具名 git add,未將 marker 帶入 PR。
  • .html 被 coverage 排除,因此 mirror 測試只作決策層輔助;production correctness 由 WT-1~WT-9 provenance-bound 手動 gate。

Closes #162

cheerc added 9 commits July 20, 2026 19:34
Closes t-20260720112615791999-29953-14

Co-Authored-By: Claude <noreply@anthropic.com>

Agend-Agent: cb-team-impl
Agend-Task: t-20260720112615791999-29953-14
Agend-Branch: feat/162-week-view-time-axis
Agend-Issued-At: 2026-07-20T11:28:30.450120+00:00
Closes t-20260720112615791999-29953-14

Co-Authored-By: Claude <noreply@anthropic.com>

Agend-Agent: cb-team-impl
Agend-Task: t-20260720112615791999-29953-14
Agend-Branch: feat/162-week-view-time-axis
Agend-Issued-At: 2026-07-20T11:28:30.450120+00:00
Closes t-20260720112615791999-29953-14

Co-Authored-By: Claude <noreply@anthropic.com>

Agend-Agent: cb-team-impl
Agend-Task: t-20260720112615791999-29953-14
Agend-Branch: feat/162-week-view-time-axis
Agend-Issued-At: 2026-07-20T11:28:30.450120+00:00
Closes t-20260720112615791999-29953-14

Co-Authored-By: Claude <noreply@anthropic.com>

Agend-Agent: cb-team-impl
Agend-Task: t-20260720112615791999-29953-14
Agend-Branch: feat/162-week-view-time-axis
Agend-Issued-At: 2026-07-20T11:28:30.450120+00:00
Closes t-20260720112615791999-29953-14

Co-Authored-By: Claude <noreply@anthropic.com>

Agend-Agent: cb-team-impl
Agend-Task: t-20260720112615791999-29953-14
Agend-Branch: feat/162-week-view-time-axis
Agend-Issued-At: 2026-07-20T11:28:30.450120+00:00
Closes t-20260720112615791999-29953-14

Co-Authored-By: Claude <noreply@anthropic.com>

Agend-Agent: cb-team-impl
Agend-Task: t-20260720112615791999-29953-14
Agend-Branch: feat/162-week-view-time-axis
Agend-Issued-At: 2026-07-20T11:28:30.450120+00:00
Closes t-20260720112615791999-29953-14

Co-Authored-By: Claude <noreply@anthropic.com>

Agend-Agent: cb-team-impl
Agend-Task: t-20260720112615791999-29953-14
Agend-Branch: feat/162-week-view-time-axis
Agend-Issued-At: 2026-07-20T11:28:30.450120+00:00
Closes t-20260720112615791999-29953-14

Co-Authored-By: Claude <noreply@anthropic.com>

Agend-Agent: cb-team-impl
Agend-Task: t-20260720112615791999-29953-14
Agend-Branch: feat/162-week-view-time-axis
Agend-Issued-At: 2026-07-20T11:28:30.450120+00:00
Closes t-20260720112615791999-29953-14

Co-Authored-By: Claude <noreply@anthropic.com>

Agend-Agent: cb-team-impl
Agend-Task: t-20260720112615791999-29953-14
Agend-Branch: feat/162-week-view-time-axis
Agend-Issued-At: 2026-07-20T11:28:30.450120+00:00
@cheerc
cheerc marked this pull request as ready for review July 20, 2026 11:53
@cheerc

cheerc commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

VERIFIED(secondary review)

Reviewer: cb-team-reviewer2(第二審/獨立判斷)
Reviewed head: 0b1fd82c197d14601133ec93bb2dd1301c89fe98
Depth: D3(full-tree + static evidence)
Lenses: correctness
Audit mode: full


Summary

經完整對照 plan @ 2207b77、design @ f3ec3cb 與 PR #163 diff,本實作正確滿足設計 §8 全部 9 條驗收條件(WT 手動驗證除外)。plan 的 Key Constraints 9 條全部滿足。lead 預驗 4 項疑點均有合理答案。無 CRITICAL / WARNING findings。


Lead 疑點回應

A1. resolveRenderTarget production 與 mirror 簽名不同
Production 版回字串、mirror 版回 {renderer, shouldFilter}。差異合理:production 的 renderScheduleTable 在呼叫 resolveRenderTarget 之前已處理 ALL_SCHEDULES_ID early-return 和 filterDataByActiveFilters,故 production 版不需 shouldFilter / allSchedules 分支。mirror 版是獨立純函式需自行判斷 filter/allSchedules。同源宣稱成立(核心 dispatch 邏輯一致)。

A2. Day PDF 的 bottomText 未走共享 helper
PDFExport.js.html:164 day view 分支仍 inline。這是刻意的:day 不支援 time sort 的 bottomObj(day-time 是一課一列、左欄教室),resolvePdfBottomText 的 time 分支格式 老師 · 教室 不適用於 day PDF。兩套格式語意不同,不會 drift。

A3. resolveRestoredSort 未傳 weekMode
Production 版(ViewDecisionHelpers.js.html)的 resolveRestoredSort 只宣告 3 參數(無 weekMode),mirror 版宣告 4 參數(含未使用的 weekMode)。行為一致:兩者邏輯都是 currentViewMode === dayMode → time,不依賴 weekMode。mirror 多餘參數是 minor 不一致但不影響正確性。髒值/第三種值情境:lastViewMode 髒值在既有程式碼中就會造成問題(JavaScript.html:26 直接讀 localStorage),非本 PR 引入。

A4. 空 cell guard 只加註解
Interaction.js.html:549 既有 guard if (!classroom || isNaN(day)) return; 已正確攔截。time-grid 的 <td> 只設 data-day、不設 data-classroomclassroomundefined!classroom early return ✅。註解只是文件化既有行為。


Plan Key Constraints 對照

# Constraint 狀態 Evidence
1 日檢視完全不變 setViewMode day branch 維持 timerenderDayViewByTime 輸出結構不變;day restore 強制 time
2 持久化 view-scoped setViewSortModelastViewSortModeresolveRestoredSort day→time、week→stored
3 flatten/group 四 consumer 同源 _flattenCoursesForDays 被 4 處共用(renderWeekViewByTimerenderDayViewByTimerenderByTeacherrenderAllSchedulesView
4 restore 不用 UI module helper;全域載入早於 JavaScript ViewDecisionHelpersIndex.html 中先於 UI.js.htmlJavaScript 載入
5 徽章 day-only DataCollection.js.html producer guard 未改;JavaScript.html timer 未改
6 PDF 對角標籤 week guard resolvePdfDiagonalLabelcurrentViewMode === weekMode guard
7 空 cell 無 data-classroom;課程卡保留 dataset renderWeekViewByTime td 只設 data-daycreateClassElement 保留 dataset.classroom/dataset.day
8 mirror-only 紀律 tests/lib/uiHelpers.js 檔頭有 mirror-only 標註
9 分支 A 選擇理由成立 IIFE 形式、.eslintrc.json globals 同步、Index.html 載入順序正確

Impl 自報 deviation 確認

C1. 分支 A(IIFE 全域)ViewDecisionHelpers.js.html 為標準 IIFE,.eslintrc.json 已加 "ViewDecisionHelpers": "readonly"Index.html 載入順序正確(先於 consumer)。✅

C2. 「移除 resolvePdfBottomText → 4 tests fail」反向驗證:此驗證只證明 mirror 函式被測試 import,不證明 production 的 PDFExport.js.html week-time 分支真的依賴它。這是 mirror-only 架構的已知 false-green 限制(設計 §7.1 blocker 4),已由 WT-5 手動案例 gate。impl 誠實揭露此限制,不構成 defect。


Design §8 驗收對照

# 驗收 狀態 備註
1 週檢視「依時間」鈕可見可按 AppElements.daySortTimeBtn.style.display = 'block'
2 左欄時間軸、橫向週一~日 renderWeekViewByTime 正確實作時間軸網格
3 卡片老師+教室 timeSort context → showTeacher: true, showClassroom: true
4 可編輯/刪除/不可拖拉/無新增 time-grid 空 cell 無 data-classroom;課程卡保留 dataset
5 refresh 仍 week+time lastViewSortMode 持久化 + resolveRestoredSort
6 全部課表唯讀時間軸 renderAllSchedulesView 有 week+time 唯讀分支
7 PDF 老師+教室、標「星期/時間」 resolvePdfBottomText + resolvePdfDiagonalLabel
8 空 cell 不開新增 `!classroom
9 日檢視不變 day restore 強制 timerenderDayViewByTime 輸出結構不變

Reviewed Files

  • .eslintrc.json — globals 同步 ✅
  • Index.html — 載入順序正確 ✅
  • Interaction.js.html — 空 cell guard 正確 ✅
  • JavaScript.html — constructor restore 正確 ✅
  • PDFExport.js.html — week-time PDF 分支 + 對角標籤 ✅
  • TestCases.md — WT-1~WT-9 手動案例 ✅
  • UI.js.html — render dispatch / renderWeekViewByTime / renderAllSchedulesView week+time / flatten helper ✅
  • ViewDecisionHelpers.js.html — 新 IIFE 全域 ✅
  • tests/lib/integrationHelpers.js — 契約加入新 render 函式 ✅
  • tests/lib/uiHelpers.js — mirror 同步 + mirror-only 標註 ✅
  • tests/unit/uiHelpers.test.js — 測試覆蓋 ✅
  • docs/verification-checklist-2026-07-20-week-view-time-axis.md — 驗證清單 ✅

Stage 2 Adversarial Audit

<stage2_audit>

  • 高風險 hunk:

    1. UI.js.html renderWeekViewByTime(新函式,時間軸網格核心)
    2. UI.js.html renderAllSchedulesView week+time 分支(全部課表唯讀變體)
    3. ViewDecisionHelpers.js.html resolveRestoredSort(持久化 restore 邏輯)
  • 破壞情境結論:

    1. renderWeekViewByTime 空資料 → _flattenCoursesForDays[]_groupCoursesByStartTime[] → 不產列 ✅。dataToRender[classroom][day]nullif (list) false → 跳過 ✅
    2. renderAllSchedulesView week+time 分支:全部課表 + 篩選後全空 → groups[] → 不產列 ✅
    3. resolveRestoredSortlastViewMode 髒值 → 走 week 分支 → storedSort fallback 到 classroom → 安全降級 ✅
  • lenses 合約:correctness — 已過一輪(ecc-code-reviewer 方法論)

  • 盲點自問:「若本 PR 有一個我沒抓到的 bug,它最可能藏在哪?」
    renderAllSchedulesView 的 week+time 分支與 renderWeekViewByTime 幾乎重複,但全部課表模式下 dataToRender key 為 [scheduleName] classroomName_flattenCoursesForDays 用 key 作 classroom 欄位傳入 createClassElementdataset.classroom 含 prefix。但全部課表模式為唯讀,dataset.classroom 不影響操作。既有 day+time 分支行為相同。非 bug。
    </stage2_audit>


Evidence

ran: gh pr diff 163 --repo cheerc/classroombooking → 12 files changed, 520 insertions(+), 56 deletions(-)
ran: rg '^(<<<<<<<|=======|>>>>>>>)' -g '!*.md' -g '!docs/**' --repo cheerc/classroombooking → no output
ran: gh pr checks 163 --repo cheerc/classroombooking --json name,bucket,state → validate (pass, SUCCESS)
ran: gh pr view 163 --json headRefOid0b1fd82 matches dispatch head_sha
cited: ViewDecisionHelpers.js.html:14-16resolveRestoredSort: function({ currentViewMode, storedSort, dayMode }) { if (currentViewMode === dayMode) return 'time'; ... }
cited: UI.js.html:218-222renderWeekViewByTime 使用 this._flattenCoursesForDays + this._groupCoursesByStartTime
cited: UI.js.html:129-130const { showTeacher, showClassroom } = ViewDecisionHelpers.resolveClassElementFields(viewContext);
cited: Interaction.js.html:549if (!classroom || isNaN(day)) return;
cited: JavaScript.html:28-32ViewDecisionHelpers.resolveRestoredSort({ currentViewMode: ..., storedSort: ..., dayMode: AppConfig.MODES.DAY })
cited: PDFExport.js.html:67-71ViewDecisionHelpers.resolvePdfDiagonalLabel({ currentViewMode: ..., viewSortMode: ..., weekMode: AppConfig.MODES.WEEK })
cited: PDFExport.js.html:85-89ViewDecisionHelpers.resolvePdfBottomText({ viewSortMode: ..., teacher: ..., classroom: ... })
cited: tests/lib/uiHelpers.js:3-6 — mirror-only 標註

@cheerc

cheerc commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

REJECTED — PR #163 @ 0b1fd82

Scope: 2207b77..0b1fd82; D3 full-scan; correctness; all 12 changed files; plan/design are the implementation baseline.

Finding [MEDIUM] — Branch A shared render dispatch does not cover ALL_SCHEDULES.
File: UI.js.html:517-520, 590-630; ViewDecisionHelpers.js.html:16-21
Issue: renderScheduleTable returns to renderAllSchedulesView before invoking ViewDecisionHelpers. The all-schedules renderer then owns a separate DAY/WEEK/time if-chain. The extracted resolver has no activeScheduleId input, while tests/lib/uiHelpers.js resolves activeScheduleId === allSchedulesId to a separate renderer object. This is not the same authored decision path required by the plan/design.
Concrete failure: currentViewMode is copied directly from localStorage. If a stale unsupported value such as preview reaches state with viewSortMode time, the normal path resolves weekTime, but the all-schedules path matches neither DAY nor WEEK and falls through to its classroom renderer. Any future shared-dispatch change also bypasses all-schedules.
Fix: make one shared render decision cover the all-schedules case and have both production paths consume it; align the mirror contract and add a regression case for all-schedules plus unsupported mode (or validate the mode before dispatch).

Stage-2 disposition:

  • Day PDF bottomText remains inline intentionally; it matches the base day branch, while the shared helper is used by the new week branch.
  • Empty time-grid cells in both normal and all-schedules week-time renderers set only data-day; the existing handler returns when classroom is absent.
  • The claimed reverse test for removing resolvePdfBottomText exercises tests/lib/uiHelpers.js only. It does not prove the production PDF branch; WT-5 remains the required production check. Per dispatch, missing WT-1..WT-9 evidence is not used as a separate rejection reason.
  • The added verification checklist still says PR 待建立 and Verified-at 8108110; do not treat it as current-head production evidence.

reviewed_head: 0b1fd82
commands: gh pr checks; gh pr view; gh api exact-head source reads; gh pr diff conflict-marker scan. No product tests run by reviewer.
phase_files_read: phase-1-startup-fastpath.md, phase-2-scope-sha.md, phase-3-stage1-correctness.md, phase-4-stage2-adversarial.md, phase-5-verdict-mirror.md, phase-6-report-evidence.md, plan-review-lane.md; ecc-code-reviewer/SKILL.md; classroombooking REVIEWER.md and PROJECT.md.

Evidence

ran: gh pr checks 163 --repo cheerc/classroombooking --json name,state,bucket,link → [{"bucket":"pass","link":"https://github.com/cheerc/classroombooking/actions/runs/29739690585/job/88343302904","name":"validate","state":"SUCCESS"}]
ran: gh pr view 163 --repo cheerc/classroombooking --json headRefOid,baseRefOid,mergeStateStatus,files → baseRefOid=2207b772df29a801cdd5aa08c81ddc1d47ddcc3a; headRefOid=0b1fd82c197d14601133ec93bb2dd1301c89fe98; mergeStateStatus=BLOCKED; 12 changed files
ran: gh pr diff 163 --repo cheerc/classroombooking --patch | rg conflict markers → no output
cited: UI.js.html:517 — if (app.activeScheduleId === AppConfig.ALL_SCHEDULES_ID) {
cited: UI.js.html:518 — this.renderAllSchedulesView();
cited: UI.js.html:519 — return;
cited: UI.js.html:529 — const renderer = ViewDecisionHelpers.resolveRenderTarget({
cited: UI.js.html:590 — if (app.currentViewMode === AppConfig.MODES.DAY && app.viewSortMode === 'time') {
cited: UI.js.html:609 — } else if (app.currentViewMode === AppConfig.MODES.WEEK && app.viewSortMode === 'time') {
cited: UI.js.html:630 — } else {
cited: ViewDecisionHelpers.js.html:16 — resolveRenderTarget: function({ currentViewMode, viewSortMode, dayMode }) {
cited: ViewDecisionHelpers.js.html:19 — if (currentViewMode === dayMode && viewSortMode === 'time') return 'time';
cited: ViewDecisionHelpers.js.html:20 — if (viewSortMode === 'time') return 'weekTime';
cited: tests/lib/uiHelpers.js:36 — if (activeScheduleId === allSchedulesId) {
cited: tests/lib/uiHelpers.js:37 — return { renderer: 'allSchedules', shouldFilter: false };
cited: JavaScript.html:26 — currentViewMode: localStorage.getItem('lastViewMode') || AppConfig.MODES.WEEK,
cited: JavaScript.html:30 — dayMode: AppConfig.MODES.DAY
cited: Interaction.js.html:548 — const classroom = cell.dataset.classroom;
cited: Interaction.js.html:551 — if (!classroom || isNaN(day)) return;
cited: UI.js.html:619 — td.dataset.day = day;
cited: UI.js.html:703 — td.dataset.day = day;

cheerc added 2 commits July 20, 2026 20:12
Closes t-20260720112615791999-29953-14

Co-Authored-By: Claude <noreply@anthropic.com>

Agend-Agent: cb-team-impl
Agend-Task: t-20260720112615791999-29953-14
Agend-Branch: feat/162-week-view-time-axis
Agend-Issued-At: 2026-07-20T11:28:30.450120+00:00
Closes t-20260720112615791999-29953-14

Co-Authored-By: Claude <noreply@anthropic.com>

Agend-Agent: cb-team-impl
Agend-Task: t-20260720112615791999-29953-14
Agend-Branch: feat/162-week-view-time-axis
Agend-Issued-At: 2026-07-20T11:28:30.450120+00:00
@cheerc

cheerc commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

Rework update @ 76f6ca1fc19f96986b8261d071505bdc7db2d450

Fixed the primary review finding: the shared render decision now computes scheduleRenderer for both normal and ALL_SCHEDULES_ID paths. renderScheduleTable resolves once before the all-schedules early return, and renderAllSchedulesView consumes that same decision. Unsupported currentViewMode + time now consistently selects the week-time renderer instead of silently falling back to classroom mode.

Added mirror regression coverage for all-schedules + unsupported mode and aligned the factory decision contract. Updated the independent verification checklist to PR #163 and the code verification head 9b86677 (the checklist commit itself advances the PR head).

Evidence

  • npm test -- uiHelpers → 53 passed
  • npm test -- integrationHelpers → 20 passed
  • ./workflow.sh at 76f6ca1 → t1,t2,t3,t4,t5,t7,t8,t9 all PASS (8/8)
  • Full Vitest → 37 files, 1188 tests passed
  • PHPUnit → 60 tests, 117 assertions passed
  • CI will re-run for the pushed head; WT-1~WT-9 remain post-merge operator verification per scope decision.

@cheerc

cheerc commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

VERIFIED(R2 secondary re-review)

Reviewer: cb-team-reviewer2(第二審 R2)
Reviewed head: 76f6ca1fc19f96986b8261d071505bdc7db2d450
Delta: 0b1fd82..76f6ca1 — 5 files, +100/-35
Depth: D3(full-tree + static evidence)
Lenses: correctness


Summary

R2 delta 正確修復了 R1 的 finding(all-schedules dispatch 不經共享決策物件)。經完整對照 plan、design 與完整 diff(base..head),無新的 CRITICAL/WARNING findings。


A. R1 finding 修復確認

primary 描述的失敗情境currentViewMode='preview' + viewSortMode='time' → normal path 回 'weekTime'、all-schedules 落 else 走 classroom。

R2 修復機制

  1. renderScheduleTableUI.js.html:517-525)在 ALL_SCHEDULES_ID early-return 之前統一呼叫 resolveRenderTarget,得到 { renderer, scheduleRenderer }
  2. renderAllSchedulesViewUI.js.html:558)接受 scheduleRenderer 參數,if-chain 改為 scheduleRenderer === 'time' / scheduleRenderer === 'weekTime'
  3. resolveRenderTargetViewDecisionHelpers.js.html:16-33)回傳 { renderer, scheduleRenderer },all-schedules 與 normal 路徑共用同一份 scheduleRenderer 推導邏輯

驗證currentViewMode='preview' + viewSortMode='time' + activeScheduleId=ALL_ID

  • resolveRenderTarget{ renderer: 'allSchedules', scheduleRenderer: 'weekTime' }
  • renderAllSchedulesView('weekTime') → 走 scheduleRenderer === 'weekTime' 分支 ✅
  • normal path(非 all-schedules)→ renderer: 'weekTime'renderWeekViewByTime
  • 兩條路徑一致

其他 state 組合分歧檢查:窮舉 scheduleRenderer ∈ {'classroom','teacher','time','weekTime'}

  • 'classroom' → normal: renderByClassroom, all-schedules: else(classroom 遍歷)✅
  • 'teacher' → normal: renderByTeacher, all-schedules: else(classroom 遍歷)⚠️ 既有分歧(all-schedules 需 schedule prefix,renderByTeacher 無 prefix),非 R2 引入
  • 'time' → normal: renderDayViewByTime, all-schedules: scheduleRenderer === 'time'
  • 'weekTime' → normal: renderWeekViewByTime, all-schedules: scheduleRenderer === 'weekTime'

B. Delta 對抗審查

B1. renderAllSchedulesView 參數 fallback(UI.js.html:559-567

if (!scheduleRenderer) {
    const renderTarget = ViewDecisionHelpers.resolveRenderTarget({...});
    scheduleRenderer = renderTarget.scheduleRenderer;
}
  • 唯一 caller(:525)有傳參數 → fallback 正常不走
  • fallback 邏輯與主路徑一致(同一份 resolveRenderTarget)→ 即使走到也 safe ✅

B2. 簽名變更的呼叫端全覆蓋

Production 呼叫端(UI.js.html):

  • :517renderScheduleTable ✅(傳 5 參數,用 renderTarget.scheduleRenderer
  • :560renderAllSchedulesView fallback ✅(傳 5 參數,用 renderTarget.scheduleRenderer

Mirror 測試端(tests/unit/uiHelpers.test.js):

  • 全部 9 個 resolveRenderTarget 測試已更新為 { renderer, scheduleRenderer, shouldFilter } 格式 ✅

shouldFilter 差異:mirror 有 shouldFiltertests/lib/uiHelpers.js:36-38),production 沒有。這是合理的——production 在 resolveRenderTarget 呼叫之後才做 filterDataByActiveFilters。不影響 dispatch 正確性。

B3. Mirror 契約同形

Production(ViewDecisionHelpers.js.html:16-33)與 mirror(tests/lib/uiHelpers.js:26-57)的 resolveRenderTarget 核心邏輯(scheduleRenderer 推導的 if-chain + renderer 決定)完全一致 ✅。唯一差異是 mirror 多了 shouldFilteractiveFilters 解構。

B4. 既有測試迴歸

簽名從 { renderer: string } 改為 { renderer, scheduleRenderer, shouldFilter }。所有舊測試已更新(每個 toEqual 都包含 scheduleRenderer)。CI 綠 ✅。


Plan Key Constraints 逐條對照(完整 diff base..head)

# Constraint 如何被滿足
1 日檢視完全不變 setViewMode day branch 維持 timeUI.js.html:405);renderDayViewByTime 輸出結構不變(UI.js.html:640-660);day restore 強制 timeViewDecisionHelpers.js.html:12
2 持久化 view-scoped setViewSortModelastViewSortModeUI.js.html:425);resolveRestoredSort day→time、week→storedSort(ViewDecisionHelpers.js.html:11-14
3 flatten/group 四 consumer 同源 _flattenCoursesForDays 被 4 處共用(UI.js.html:598/:607/:626/:643)。R2 修復:dispatch 決策也統一由 resolveRenderTarget 決定(:517-525),renderAllSchedulesView 不再有自有 if-chain(:604/:623
4 restore 不用 UI module helper;全域載入早於 JavaScript ViewDecisionHelpersIndex.html 中先於 UI.js.htmlJavaScript 載入;JavaScript.html:28-32 直接呼叫 ViewDecisionHelpers.resolveRestoredSort
5 徽章 day-only DataCollection.js.html producer guard 未改;JavaScript.html timer 未改
6 PDF 對角標籤 week guard resolvePdfDiagonalLabelcurrentViewMode === weekMode guard(ViewDecisionHelpers.js.html:40-43
7 空 cell 無 data-classroom;課程卡保留 dataset renderWeekViewByTime td 只設 data-dayUI.js.html:629);createClassElement 保留 dataset.classroom/dataset.dayUI.js.html:141-144
8 mirror-only 紀律 tests/lib/uiHelpers.js:1-6 有 mirror-only 標註
9 分支 A 選擇理由成立 IIFE 形式(ViewDecisionHelpers.js.html:6)、.eslintrc.json globals 同步、Index.html 載入順序正確

Reviewed Files(R2 delta)

  • UI.js.htmlrenderScheduleTable early-return 重構 + renderAllSchedulesView 參數化 ✅
  • ViewDecisionHelpers.js.htmlresolveRenderTarget 簽名改為 5 參數 + 回傳 { renderer, scheduleRenderer }
  • tests/lib/uiHelpers.js — mirror 對齊 production 的 resolveRenderTarget
  • tests/unit/uiHelpers.test.js — 既有測試更新 + 新增 regression test(all-schedules + unsupported mode)✅
  • docs/verification-checklist-...md — PR# + SHA 更新 ✅

Stage 2 Adversarial Audit

<stage2_audit>

  • 高風險 hunk:

    1. UI.js.html:517-525renderScheduleTable early-return 重構(resolveRenderTarget 移至 early-return 前)
    2. UI.js.html:558-567renderAllSchedulesView 參數 fallback
    3. ViewDecisionHelpers.js.html:16-33resolveRenderTarget 新簽名
  • 破壞情境結論:

    1. resolveRenderTarget 拋錯 → renderScheduleTable 的 try/catch 攔截 → showNotification('渲染課程表失敗') → 安全降級 ✅
    2. renderAllSchedulesView 未傳參數 → fallback 重新解析 → 與主路徑一致 ✅
    3. 呼叫端誤用 renderTarget.renderer 而非 renderTarget.scheduleRenderer 做 render dispatch → renderer === 'allSchedules'scheduleRenderer 才是正確 dispatch key。但 production 已正確使用 renderTarget.scheduleRenderer:536)✅
  • lenses 合約:correctness — 已過一輪

  • 盲點自問:「若本 PR 有一個我沒抓到的 bug,它最可能藏在哪?」
    renderAllSchedulesViewelse 分支(:643 onward)仍使用 app.currentViewMode 決定 daysToRender。若 currentViewMode 是髒值(如 'preview'),daysToRender 會是 [app.currentDayIndex](單日)而非 7 天。但 normal path 的 renderByClassroom 也使用 app.currentViewMode,兩者行為一致。非 R2 delta 引入。
    </stage2_audit>


Evidence

ran: gh api 'repos/cheerc/classroombooking/compare/0b1fd82...76f6ca1' --jq '.files[] | .patch' → 5 files, +100/-35
ran: gh pr checks 163 --repo cheerc/classroombooking --json name,bucket,state → validate (pass, SUCCESS)
ran: gh pr view 163 --json headRefOid76f6ca1 matches dispatch head_sha
ran: rg '^(<<<<<<<|=======|>>>>>>>)' -g '!*.md' -g '!docs/**' → no output
cited: ViewDecisionHelpers.js.html:16-33resolveRenderTarget 新簽名:5 參數、回傳 { renderer, scheduleRenderer }
cited: ViewDecisionHelpers.js.html:31renderer: activeScheduleId === allSchedulesId ? 'allSchedules' : scheduleRenderer
cited: UI.js.html:517-525renderScheduleTable 在 early-return 前統一呼叫 resolveRenderTarget
cited: UI.js.html:525this.renderAllSchedulesView(renderTarget.scheduleRenderer) — 傳遞 scheduleRenderer
cited: UI.js.html:558-567renderAllSchedulesView 接受 scheduleRenderer 參數 + fallback
cited: UI.js.html:604if (scheduleRenderer === 'time') — all-schedules 使用傳入的 scheduleRenderer
cited: UI.js.html:623else if (scheduleRenderer === 'weekTime') — all-schedules 使用傳入的 scheduleRenderer
cited: tests/lib/uiHelpers.js:26-57 — mirror resolveRenderTarget 與 production 同形
cited: tests/unit/uiHelpers.test.js:102-113 — regression test: all-schedules + unsupported mode → scheduleRenderer: 'weekTime'

@cheerc

cheerc commented Jul 20, 2026

Copy link
Copy Markdown
Owner Author

VERIFIED — PR #163 @ 76f6ca1

Scope: base 2207b77..head 76f6ca1; R2 delta 0b1fd82..76f6ca1; D3 full-scan; correctness; 12-file PR plus 5-file delta.

R1 fix verified: resolveRenderTarget now computes one scheduleRenderer for normal and ALL_SCHEDULES states, renderScheduleTable resolves before the all-schedules early return, and renderAllSchedulesView consumes the passed renderer. The unsupported-mode + time regression is covered in the mirror and maps both paths to weekTime.

D9 adversarial results:

  • Fallback renderAllSchedulesView(scheduleRenderer) recomputes the same resolver only when no argument is supplied. Exact-head caller scan found the production call with the argument plus the method definition; no second production caller reaches the fallback.
  • All production and mirror resolver callers use the new object shape. No old three-argument production/test call remains in the exact-head source scan.
  • The mirror retains shouldFilter as a mirror-only outer-filter field. Production does not consume it; normal and all-schedules production paths apply filterDataByActiveFilters at their own renderer entry points. This is a non-blocking contract/watchpoint, not a dispatch divergence.
  • Full base diff rechecked against plan Architecture/Global Constraints: global include order, constructor restore timing, four uiModule flatten/group receivers, day-only upcoming producer/timer, day PDF behavior, readonly all-schedules cards, and empty time-grid cells remain consistent.

Unverified/watchpoints:

  • WT-1..WT-9 still require post-merge clasp/browser evidence per operator decision; no product tests were run by reviewer.
  • The checklist records Verified-at 9b86677 and the PR body still describes verification at the prior R1 head; neither is treated as current-head production evidence. Current CI validate is green at this head.

reviewed_head: 76f6ca1
commands: gh pr checks; gh pr view; gh api compare; exact-head source reads; full-tree remote caller scan; gh pr diff conflict-marker scan. No product tests run by reviewer.
audit_mode: D3 full-scan; delta treated as new code for Stage 1 and Stage 2.
phase_files_read: phase-1-startup-fastpath.md, phase-2-scope-sha.md, phase-3-stage1-correctness.md, phase-4-stage2-adversarial.md, phase-5-verdict-mirror.md, phase-6-report-evidence.md; ecc-code-reviewer/SKILL.md; classroombooking REVIEWER.md and PROJECT.md.

Evidence

ran: gh pr checks 163 --repo cheerc/classroombooking --json name,state,bucket,link → [{"bucket":"pass","link":"https://github.com/cheerc/classroombooking/actions/runs/29741432548/job/88348855980","name":"validate","state":"SUCCESS"}]
ran: gh api compare/0b1fd82c197d14601133ec93bb2dd1301c89fe98...76f6ca1 → status=ahead; base_commit=0b1fd82c197d14601133ec93bb2dd1301c89fe98; merge_base=0b1fd82c197d14601133ec93bb2dd1301c89fe98; 5 changed files
ran: exact-head full-tree remote scan for ViewDecisionHelpers, resolveRenderTarget, renderAllSchedulesView, scheduleRenderer → output included Index.html, JavaScript.html, PDFExport.js.html, UI.js.html, ViewDecisionHelpers.js.html, tests/lib/uiHelpers.js, tests/lib/integrationHelpers.js only
ran: gh pr diff 163 --repo cheerc/classroombooking --patch | rg conflict markers → no output
cited: ViewDecisionHelpers.js.html:16 — resolveRenderTarget: function({ activeScheduleId, allSchedulesId, currentViewMode, viewSortMode, dayMode }) {
cited: ViewDecisionHelpers.js.html:31 — renderer: activeScheduleId === allSchedulesId ? 'allSchedules' : scheduleRenderer,
cited: ViewDecisionHelpers.js.html:32 — scheduleRenderer
cited: UI.js.html:517 — const renderTarget = ViewDecisionHelpers.resolveRenderTarget({
cited: UI.js.html:524 — if (renderTarget.renderer === 'allSchedules') {
cited: UI.js.html:525 — this.renderAllSchedulesView(renderTarget.scheduleRenderer);
cited: UI.js.html:536 — const renderer = renderTarget.scheduleRenderer;
cited: UI.js.html:558 — renderAllSchedulesView: function(scheduleRenderer) {
cited: UI.js.html:560 — const renderTarget = ViewDecisionHelpers.resolveRenderTarget({
cited: UI.js.html:604 — if (scheduleRenderer === 'time') {
cited: UI.js.html:623 — } else if (scheduleRenderer === 'weekTime') {
cited: UI.js.html:644 — } else {
cited: tests/lib/uiHelpers.js:53 — return {
cited: tests/lib/uiHelpers.js:54 — renderer: activeScheduleId === allSchedulesId ? 'allSchedules' : scheduleRenderer,
cited: tests/lib/uiHelpers.js:55 — scheduleRenderer,
cited: tests/unit/uiHelpers.test.js:102 — it('routes all-schedules plus unsupported view mode to the shared week-time renderer', () => {
cited: tests/unit/uiHelpers.test.js:109 — renderer: 'allSchedules',
cited: tests/unit/uiHelpers.test.js:110 — scheduleRenderer: 'weekTime',
cited: tests/unit/uiHelpers.test.js:111 — shouldFilter: false,
cited: Index.html:473 —
cited: JavaScript.html:27 — viewSortMode: ViewDecisionHelpers.resolveRestoredSort({
cited: UI.js.html:530 — if (app.activeFilters && app.activeFilters.length > 0) {
cited: UI.js.html:531 — dataToRender = app.filterDataByActiveFilters(dataToRender);
cited: UI.js.html:600 — if (app.activeFilters && app.activeFilters.length > 0) {
cited: UI.js.html:601 — dataToRender = app.filterDataByActiveFilters(dataToRender);

@cheerc
cheerc merged commit a0fdefb into main Jul 20, 2026
1 check passed
@cheerc
cheerc deleted the feat/162-week-view-time-axis branch July 20, 2026 12:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

週檢視新增「依時間」時間軸模式

1 participant