Skip to content

Tech Debt Register

追蹤 @followup[TD-NNN] marker 對應的未解決項目。所有在 openspec/changes/**/tasks.md 裡出現的 marker 都必須在此有對應 entry,否則 spectra-archive 會被 pre-archive-followup-gate.sh 攔截。

規則詳見 .claude/rules/follow-up-register.md


Index

IDTitlePriorityStatusDiscoveredOwner
TD-001mcp-token-store libsql 不相容lowdone2026-04-20 B16 #10
TD-002guest_policy DB-direct UPDATE 造成 cache driftmiddone2026-04-20 B16 #7
TD-003text-dimmed 對比度不足(cross-change residual)middone2026-04-20 B17 C#11.9
TD-004首頁 Google login button 高度 36px < WCAG 40pxhighdoneB17 viewport-baseline.spec.ts
TD-005Admin 頁面 a11y violations 批次(@nuxt/a11y 首輪掃描)highdone2026-04-21 RAF @nuxt/a11y
TD-006Nuxt UI subtle variant tonal badge 對比度不足middone2026-04-20 TD-003 e2e exclude
TD-007裝飾 icon tonal color 低於 WCAG 1.4.11 non-text AAlowdone2026-04-20 TD-006 review
TD-008acceptance-tc-0x MCP 整合測試在 TD-001 修後破損middone2026-04-20 add-ai-gateway
TD-009user_profiles.email_normalized 全面改 nullablemiddone2026-04-21 passkey-authentication
TD-010credentials / admin-members endpoint libsql 不相容middone2026-04-21 passkey §16 DR
TD-011migration 0009 FK cascade 設計不符 self-delete / audithighdone2026-04-21 passkey §17.8
TD-012passkey-first → link Google 被 better-auth email 檢驗擋住highdone2026-04-21 passkey §17.3
TD-013/account/settings 新增 passkey 缺 naming dialoglowdone2026-04-21 passkey §17.2
TD-014error-sanitizer 後 12 test 抛 evlog Logger not initmiddone2026-04-21 drizzle-refactor apply
TD-015SSE 長連線缺 heartbeat,30s proxy timeout 風險middone2026-04-24 /commit review
TD-016isAbortError / createAbortError 在四處重複實作lowdone2026-04-24 /commit review
TD-017chat.post.ts 兩個 AI binding getter 可合併lowdone2026-04-24 /commit review
TD-018Container.vue classifyError 巢狀條件抽 lookup tablelowdone2026-04-24 /commit review
TD-019SSE reader pattern 在 client/server 雷同可抽共用lowdone2026-04-24 /commit review
TD-020CHATGPT_CONNECTOR_OAUTH_PATH_PATTERN 可收緊字元集lowdone2026-04-24 /commit review
TD-021ConversationHistory bucket toggle 缺 aria-expanded 等lowdone2026-04-24 /commit review
TD-022groupedConversations computed 不跨 midnight 重新分組lowdone2026-04-24 /commit review
TD-023index.vue 雙 LazyChatConversationHistory 產生重複 fetchlowdone2026-04-24 /commit review
TD-024chat-history-sidebar test suite 品質(string contract/resolves)lowdone2026-04-24 /commit review
TD-025Container.vue $csrfFetch.native 跳過 CSRF header 造成 /api/chat 403highdone2026-04-24 code-quality-review-followups 人工檢查 10.x
TD-026index.vue 與 ConversationHistory fallback 重複 config + refresh 邏輯lowdone2026-04-24 code-quality-review-followups /commit 0-A
TD-027MCP connector first-time authorization journey 實測待部署後驗證midopen2026-04-24 auth-redirect-refactor 人工檢查 7.4
TD-028DeleteAccountDialog Google reauth 無 callbackURL,dialog 會 unmountmiddone2026-04-25 fix-delete-account-dialog-google-reauth 人工驗證
TD-029mcp-toolkit alias fragility — shim 可能被 bypassmiddone2026-04-24 fix-mcp-streamable-http-session review MI-2
TD-030Claude.ai re-init 循環阻擋 tools/call(stateless 不足)highdone2026-04-24 fix-mcp-streamable-http-session post-deploy
TD-040Token revoke 未同步清 MCP session DOlowdone2026-04-24 upgrade-mcp-to-durable-objects Task 4.6
TD-041DO tool dispatch 未 wire up,flag=true non-initialize 回假 ackhighdone2026-04-24 upgrade-mcp-to-durable-objects Phase 4 trim
TD-042Local NuxtHub dev KV binding 未注入 cloudflare.env/mcp 503middone2026-04-24 add-mcp-tool-selection-evals 5.2 apply
TD-043Evalite afterAll 的 process.exit / throw 不 propagate 到 pnpm evallowdone2026-04-24 add-mcp-tool-selection-evals 6.5 verify
TD-044session.create.before 靜默吞 user_profiles UNIQUE 衝突 → better-auth user id 與 user_profiles.id 可能漂移middone2026-04-25 consolidate-conversation-history-config §7.4 人工檢查
TD-045Local dev bootstrap 連串斷點(narrow scope:.env AI_SEARCH_INDEX 空值 + [nuxt-hub] DB binding not found 間歇 500;migration 自動化已由 NuxtHub v0.10.7 接手)midin-progress2026-04-25 consolidate-conversation-history-config §7.4 人工檢查
TD-046agentic-rag-staging AutoRAG index 在 CF 帳號中不存在(wrangler / Notion / deploy.yml 皆引用,CF API 僅有 agentic-raghighdone2026-04-25 consolidate-conversation-history-config §7.4 人工檢查
TD-047/api/chat SSE ready 後階段 error 時 Container 未 emit conversation-persisted → DB 已建 conv 但 UI 不更新middone2026-04-25 consolidate-conversation-history-config §7.4 人工檢查
TD-048聊天 UI 缺顯式「新對話」入口 — sessionStorage 記住 active id 後只能靠刪除或 DevTools 清才能開新對話middone2026-04-25 consolidate-conversation-history-config §7.2 人工檢查
TD-049Cloudflare Pages deploy API 拒絕 git HEAD commit message(Invalid commit message UTF-8 string [8000111]middone2026-04-25 v0.43.0 deploy run 24908303837
TD-050Production / staging demo seed 已具備完整 D1 evidence、R2 metadata 與 AI Search active/current retrievalmiddone2026-05-04 production / staging mock data audit
TD-051libsql legacy_alter_table=1 與 0007/0009 RENAME-rewrite 假設衝突 → account/session/passkey FK 在 fresh local DB 殘留 user_new ref,OAuth login 報 unable_to_link_accounthighdone2026-04-25 dev server 報 unable_to_link_account
TD-052passkey-first-link-google.spec.tshubDb.transaction stub 沒覆蓋 syncUserProfile migrate path 的 tx.update().set().where() chainlowdone2026-04-25 wire-do §5.x SSE Tests cross-spec failure
TD-053fix-user-profile-id-drift production observation — 立即採樣 wrangler tail --env production 5-10 分鐘 + 撈最近 24h logs 搜 user_profiles sync failed 確認無預期外觸發lowdone2026-04-25 fix-user-profile-id-drift archive
TD-054add-new-conversation-entry-points Safari private mode 實機驗證 — archive 時授權 skip,待後續本機 Safari 補上lowopen2026-04-25 add-new-conversation-entry-points archive
TD-055TD-051 的漏網之魚:query_logs.mcp_token_id / messages.query_log_id / citation_records.query_log_id FK 在 fresh local DB 殘留 mcp_tokens_new / query_logs_new ref(migration 0010 的 RENAME-rewrite 假設失敗),任何 chat insert 直接炸 SQLITE_ERRORhighdone2026-04-26 add-sse-resilience 7.1 local heartbeat 驗證
TD-056Workers AI judge 模型 max_completion_tokens: 200 上限被截斷 → JSON parse 失敗 → pipeline_errorlowopen2026-04-26 v0.50.0 production 7.2 verify 抽查 query_logs
TD-057evlog wide event lifecycle 警告 — log.error() 在 wide event emit 後呼叫,導致 SSE stream 真實錯誤 keys 被丟棄midopen2026-04-26 production wrangler tail
TD-058Production user_profiles 6 條 orphaned rows(profile.id 不在 user.id)lowopen2026-04-26 TD-053 production 立即驗收
TD-059E2E Tests CI workflow 連續 50+ run 全紅 — nuxt preview webServer 啟動時 Node ESM loader 撞 cloudflare: protocol scheme,整個 webServer exit 1highdone2026-04-26 v0.50.1 E2E run 24940734040
TD-060Production agentic-rag AutoRAG 對 seed acceptance fixture 的 retrieval_score 平均 0.32–0.44,全部低於 directAnswerMin=0.7,治理層 100% 走 no_citation_refusehighopen2026-04-26 main-v0.0.54-acceptance run
TD-061Production query_logs r2 重測批次 28.6%(10/35)觸發 decision_path=pipeline_error;同 prompt 重複查詢可能觸發 stateful failurehighopen2026-04-26 main-v0.0.54-acceptance run
TD-062rag-query-rewriting 三個 entry point 的 retrieve closure 幾乎重複(chat.post.ts / mcp/tools/ask.ts / mcp/tools/search.ts),約 28 LoC × 3 應抽 helpermidopen2026-04-26 /commit 0-A simplify review
TD-063useRewriter: false on retry 的 docstring 在 4 個 callback signature 重複 6-9 行同一段;應只留一份 canonical 在 knowledge-query-rewriter.tslowopen2026-04-26 /commit 0-A simplify review
TD-064test/integration/retrieve-verified-evidence-with-rewriter.spec.ts 同時 mock searchresolveCurrentEvidence,違反「no mocking DB in integration tests」;應 relocate 或補真實 D1 round-trip 覆蓋 audit dynamic UPDATEmidopen2026-04-26 /commit 0-A code-review
TD-065UpdateQueryLog.rewriterStatus 型別 string | nullquery_logs.rewriter_status NOT NULL 不一致;潛在 5xxlowopen2026-04-26 /commit 0-A code-review
TD-066retrieveVerifiedEvidence=== 'success' 比對 RewriterStatus,違反專案 switch + assertNever exhaustiveness rule;新增 enum 值時不會 compiler errorlowopen2026-04-26 /commit 0-A code-review
TD-067test/tsconfig.json baseline 191 errors(component module not found + fixture type drift + Nitro route key excessive depth + allowImportingTsExtensions 缺 + middleware signature 漂移)midopen2026-05-04 clade v0.3.10 cutover pre-push test-typecheck 揭露
TD-070rag-query-rewriting 人工檢查對齊新 manual-review 規範(補 [discuss] marker + verify channel + Pre-Review Data Readiness)midopen2026-05-12 clade v1.3.6 manual-review.md 新規散播
TD-071AutoRAG → AI Search API migration:production deployed v0.57.1(2026-06-09)criticaldone2026-06-09 rag-query-rewriting acceptance blocked

TD-001 — mcp-token-store libsql 不相容

Status: done Resolved: 2026-04-20 — createMcpTokenStore() 3 個 function 遷移 Drizzle ORM(commit 1f6a4d1)+ 新增 test/integration/mcp-token-store.spec.ts 8 test cases 覆蓋 CRUD / scope / expiry / touch Priority: low Discovered: 2026-04-20 — member-and-permission-management 人工檢查 #10 Location: server/utils/mcp-token-store.ts (createMcpTokenStore() 的 createToken / findUsableTokenByHash / touchLastUsedAt — revokecreateMcpTokenAdminStore() 已是 Drizzle,不在本 TD 範圍) Related markers: search @followup[TD-001] in repo

Problem

mcp-token-store 使用 D1 $client.prepare() raw API。Local dev 用 libsql(see scripts/patch-hub-db-dev.mjs),$client 不支援 .prepare() / .bind() / .first() 等 D1-specific method,call 時拋 database.prepare is not a function

影響:

  • Local dev 無法 call /mcp endpoint 的 Bearer token 驗證流程
  • B16 人工檢查 #10(Guest MCP askKnowledge 錯誤碼)local 跑不起來,只能 production 驗
  • 未來若要 local 端寫 integration test 覆蓋 MCP 流程也會撞牆

Production (Cloudflare Workers + D1) 運作正常。

Fix approach

改用 Drizzle ORM:getDrizzleDb() 取代 getD1Database()createMcpTokenStore() 的 3 處 raw SQL 皆有對應 drizzle 表達式:

  • createTokendb.insert(schema.mcpTokens).values({...})
  • findUsableTokenByHashdb.select(...).from(schema.mcpTokens).where(and(eq, eq, eq)).limit(1) + JS 層 expires check 保留(避免跨 dialect NULL 比對語意差異)
  • touchLastUsedAtdb.update(schema.mcpTokens).set({ lastUsedAt }).where(eq(...))

同檔 createMcpTokenAdminStore()(含 revokeTokenById / listTokensForAdmin / countTokensForAdmin早已是 Drizzle,作為 canonical pattern 參考,不在本 TD 遷移範圍。其他 repo 內仍使用 getD1Database() 的 callers(server/api/* / server/mcp/tools/* / server/tasks/retention-cleanup.ts)可留作 future TD 評估。

Acceptance

  • Local pnpm dev 可 call /mcp 並通過 Bearer token 驗證(沒 database.prepare is not a function
  • 新 spec test/integration/mcp-token-store.spec.ts 覆蓋 CRUD + scope check
  • B16 人工檢查 #10 local 可重跑,GUEST_ASK_DISABLED / ACCOUNT_PENDING 皆通過

TD-002 — guest_policy DB-direct UPDATE 造成 cross-Worker cache drift

Status: done Resolved: 2026-04-20 — 選項 A 落地:新增 docs/runbooks/guest-policy.md runbook + setGuestPolicy() JSDoc 加反向說明 Priority: mid Discovered: 2026-04-20 — member-and-permission-management 人工檢查 #7(production 驗證) Location: server/utils/guest-policy.ts(cache 機制)+ operator documentation Related markers: search @followup[TD-002] in repo

Problem

setGuestPolicy() 的跨 Worker cache invalidation 依賴 KV guest_policy:version stamp:

  1. setGuestPolicy 寫 D1 system_settings + bump KV version
  2. 每個 Worker instance 下次 request getGuestPolicy() 讀 KV version → 與 cached mismatch → 重讀 D1

B#7 驗證時發現:若 admin 繞過 API 直接 DB UPDATE(例如 wrangler d1 execute DB --remote --command "UPDATE system_settings..." 或 D1 console),不會 bump KV version,結果是:

  • 已有 cached policy 的 Worker instance 繼續回舊 policy
  • 沒 cache 的 cold instance 讀到新 policy
  • 實測 5 個 parallel request 出現混合結果(2 個舊 / 3 個新)

影響:

  • Operational risk:admin 手動改 DB 做緊急 rollback 時會遇到「改了 DB 但 Worker 還回舊值」的困惑
  • 沒有告警機制

Fix approach

選項 A(文件化 + 弱提醒):

  • docs/runbooks/guest-policy.md 寫明「必須透過 PATCH /api/admin/settings/guest-policy」
  • setGuestPolicy() 的 JSDoc 加反向說明(繞過後果)

選項 B(程式層防線):

  • 每次 getGuestPolicy 讀 D1 updated_at timestamp 與 KV version 比對;若 D1 timestamp 比 KV version 新 → 強制重讀 + 自動 bump KV
  • 成本:每次 request 多讀 D1 一欄(或整 row);與原設計「KV version 快路徑」trade-off
  • 適用性:guest_policy 讀頻高(每次 chat + MCP 都 hit),原設計刻意避免 D1 讀;加這條會退化 p99

選項 C(隔離通道):

  • 拿掉 DB-direct 權限(IAM 層);所有寫入必須過 API
  • 最徹底但超過 code scope

建議:先做選項 A(低成本即見效),選項 B 視 operator 實際誤操作頻率決定。

Acceptance

  • 選項 A:docs/runbooks/guest-policy.md 建檔並在 onboard 文件引用
  • 選項 B(若採納):guest-policy.spec.ts 新增「D1 寫入繞過 API 時下次 request 會發現並自動補 bump」的 test

TD-003 — text-dimmed 對比度不足(cross-change residual)

Status: done Resolved: 2026-04-20 — 7 個檔共 13 處 text-dimmedtext-muted(commit 3a01a9f),regression guard e2e/td003-contrast.spec.ts 7/7 pass Priority: mid Discovered: 2026-04-20 — responsive-and-a11y-foundation C#11.9(axe-core 掃到三頁 color-contrast violation) Location:

  • app/pages/admin/debug/query-logs/[id].vue:180,189
  • app/components/debug/OutcomeBreakdown.vue:59
  • app/components/debug/EvidencePanel.vue:53,70,81
  • app/components/debug/LatencySummaryCards.vue:49,59,69,79,88
  • app/components/debug/ScorePanel.vue:34,49
  • app/pages/auth/callback.vue:41
  • app/components/documents/UploadWizard.vue:646,904
  • 其他未被 axe-core 初始 scan 覆蓋的頁面

Related markers: search @followup[TD-003] in repo

Problem

Nuxt UI text-dimmed token 在 dark theme 下 computed color oklch(0.556 0 0) on parent bg-default,對比度 ≈ 3.5:1(需 WCAG AA 4.5:1)。本次 session 已修 footer + chat UI 7 處(ConversationHistory / MessageList / RefusalMessage / index.vue / default.vue footer),但其他頁面約 10+ 處仍殘留。

影響:admin/debug/ 下的 logs、latency cards、auth callback、UploadWizard step state 皆有 a11y 違規;若未來掃這些頁面會報 fail。

Fix approach

批次替換 text-dimmedtext-muted(semantic 更合適,對比度充足)。需審視每處語意:

  • Muted text(「無資料」「尚未載入」etc):直接 text-muted
  • Disabled state(UploadWizard pending step):可能需 text-toned 或保留視覺區分,要 inline review

或升級 token:在 app.config.tsapp/assets/css/main.css 擴充 --ui-color-text-dimmed token 為更深色(但會影響 intended design system hierarchy)。

Acceptance

  • axe-core playwright 掃 /admin/debug/**/admin/tokens/admin/query-logs/auth/callback/admin/documents/upload 全 0 color-contrast violation
  • docs/design-review-findings.md #10 的 Cross-Change 備註更新為 resolved

TD-004 — 首頁 Google login button 高度 36px < WCAG 40px

Status: done Resolved: 2026-04-20 — app/pages/index.vue UButton 加 class="py-3"(commit b277b31) Priority: high Discovered: 2026-04-20 — responsive-and-a11y-foundation B17,e2e/viewport-baseline.spec.ts 測試既有 fail Location: app/pages/index.vue(signed-out 分支 Google login CTA) Related markers: search @followup[TD-004] in repo

Problem

viewport-baseline.spec.ts 測試 / primary CTA (Google login) 於 360×640 viewport 的 touch target size:

  • 測試期望:height ≥ 40px(內註 WCAG 2.5.5 minimum 44×44)
  • 實測:36px
  • Fail 已存在一段時間,屬 B17 既有技術債

WCAG 2.5.5 Target Size (AA) 要求 touch target ≥ 44×44 CSS px。36px 對手指觸控、運動障礙使用者是明顯風險。

Fix approach

  • 首頁 Google login button 改用較大 size prop 或加 py-3 讓高度 ≥ 44px
  • 同時掃首頁其他 interactive element 的 hit-target
  • viewport-baseline.spec.ts 驗證 pass

Acceptance

  • e2e/viewport-baseline.spec.ts 全部 pass(包含「primary CTA ≥ 40px」)
  • Chrome DevTools 手動實測 iPhone SE viewport 下 button 能輕易點到
  • 若連帶觸發其他頁面 hit-target audit 失敗,併入同一修

TD-005 — Admin 頁面 a11y violations 批次(@nuxt/a11y 首輪掃描)

Status: done Resolved: 2026-04-20 — UFormField 包裹 + srOnlyHeader utility + heading-order 全分支修復 + aria-labelledby 單一 source of truth(commit 285482b) Priority: high Discovered: 2026-04-21 — responsive-and-a11y-foundation 將社群版 nuxt-a11y 切換為官方 @nuxt/a11y@1.0.0-alpha.1,DevTools panel 首輪掃描結果 Location: /admin/query-logs/admin/documents/admin/tokens/admin/debug/latencyRelated markers: search @followup[TD-005] in repo Related findings: docs/design-review-findings.md 2026-04-21 section #13-18

Problem

@nuxt/a11y DevTools panel 首次全站掃描發現 admin 頁面群有以下違規(非 MPM / RAF scope,屬 Cross-Change DRIFT):

Critical(5 elements, 阻擋 WCAG AA)

  • /admin/query-logs — 3 × button-name:icon-only <UButton>aria-label(可能是 refresh / export / 詳情按鈕)
  • /admin/query-logs — 2 × label:form element 缺 <label> 綁定(可能是篩選器的 USelect / UInput)

Moderate(1 element)

  • /admin/debug/latencyheading-order:heading 層級跳階(例如 h1 → h3 漏 h2)

Minor(3 elements)

  • /admin/query-logs — UTable empty-table-header
  • /admin/documents — UTable empty-table-header
  • /admin/tokens — UTable empty-table-header

Scope 歸屬

  • /admin/query-logsadmin-query-log-ui capability
  • /admin/documentsadmin-document-management-ui capability
  • /admin/tokensadmin-token-management-ui capability
  • /admin/debug/latencydebug-decision-inspection capability

不併入 MPM / RAF archive(維持 scope discipline),獨立 low-friction PR 處理。

Fix approach

  1. button-name(icon-only button):給 <UButton icon="..." />aria-label="..." prop
  2. label(form element)
    • <USelect> / <UInput> 若無 visible label,加 aria-label
    • 或用 <UFormField label="..."> wrap 提供語意 label
  3. heading-order:檢查 /admin/debug/latency 的 heading 結構,補齊中間層級
  4. empty-table-header(UTable actions column):
    • 統一 pattern:header: () => h('span', { class: 'sr-only' }, '操作')
    • 參考 /admin/members 的修復(2026-04-21 已落地)作為 canonical pattern
    • 建議抽成 utility:srOnlyHeader(label: string)shared/utils/table.ts

Acceptance

  • @nuxt/a11y DevTools 對 /admin/query-logs/admin/documents/admin/tokens/admin/debug/latency critical + serious + moderate + minor 全數 0 violation
  • axe-core playwright 複掃驗證
  • docs/design-review-findings.md 2026-04-21 section #13-18 標記 ✅ Resolved

TD-006 — Nuxt UI subtle variant tonal badge 對比度不足

Status: done Resolved: 2026-04-20 — 按 nuxt/ui #1284 官方推薦做 per-component compoundVariants override in app/app.config.ts(badge / alert / button × {primary, info, success, warning, error} × {subtle, soft} 共 30 entries,text 用 -700 dark:-200 shade 達 WCAG AA)+ 附帶修 2 處 raw text-{color} (app/pages/admin/debug/query-logs/[id].vue redaction notice、app/components/chat/MessageInput.vue validation/char-count)。e2e/td003-contrast.spec.ts 移除 4 個 detail page .exclude(...) 並 7/7 pass Priority: mid Discovered: 2026-04-20 — TD-003 e2e/td003-contrast.spec.ts/admin/debug/query-logs/[id] 時 axe-core 回報,用 .exclude() 暫時排除 Location:

  • app/pages/admin/debug/query-logs/[id].vue(redaction notice <p class="text-warning">、refusal badge、pii_request badge、score badge)
  • 任何使用 bg-{color}/10 + text-{color} subtle pattern 的 UBadge / UButton / 其他 Nuxt UI 元件

Related markers: search @followup[TD-006] in repo

Problem

Nuxt UI subtle variant 的 tonal 配色在 bg-default 上對比度不足 WCAG AA 4.5:1(小字)/ 3:1(大字 / 非文字):

SelectorFG 色碼BG 色碼實測對比度WCAG AA 要求
p.text-warning (redaction notice)#f0b100#ffffff1.91:14.5:1
pii_request badge (bg-warning/10 + text-warning)#f0b100#fef7e51.78:14.5:1
超出允許範圍 refusal badge (bg-error/10 + text-error)#fb2c36#ffeaeb3.3:14.5:1
評審通過 score badge (bg-success/10 + text-success)#00c950#e5faee2.03:14.5:1

這是 Nuxt UI design system token 層級的問題,不是單一 component 使用錯誤。subtle variant 以 10% opacity tint 作 bg,text 保留原 token 的中飽和度顏色 — warning / error / success 三色的中飽和度都低於 AA 門檻。

Fix approach

可能路徑(需討論):

  1. 調整 Nuxt UI theme tokenapp.config.tsmain.css 覆蓋 --ui-color-warning / --ui-color-error / --ui-color-success 為更深色版本(例:warning.600warning.700)。影響範圍:所有用這些色 token 的 component。
  2. 換 variant:redaction notice / badge 改用 solid variant(full-saturation bg + white text)或 outline variant。代價:視覺層次變強,可能與 design 風格衝突。
  3. 升級 Nuxt UI:若上游已修,升版即可。查 changelog。
  4. 接受現狀 + 文件化 exception:在 design-review-findings.md 和 a11y report 明確宣告這些 token 組合為已知 exception;不掃 tonal badge。最差選項,但若修法成本過高可接受。

決策點:需與 design 討論哪個方向。不在當前 TD 範圍內自動選

Acceptance

  • e2e/td003-contrast.spec.ts 移除 .exclude('p.text-warning').bg-warning\\/10.bg-error\\/10.bg-success\\/10 四個排除
  • axe-core 對 detail page + 所有使用 tonal badge pattern 的頁面 color-contrast 0 violation
  • 或:明確登記 design decision 為 exception,更新 findings 標記不再追蹤

TD-007 — 裝飾 icon tonal color 低於 WCAG 1.4.11 non-text contrast

Status: done Resolved: 2026-04-20 — Audit 全 repo 14 處 <UIcon text-{color}> 使用點,全部判定為 decorative(鄰近 heading / label / status text 已表達語義,icon 僅視覺重複)。12 處加 aria-hidden="true"(2 處原本已有)+ 0 處 informational,按 WCAG 1.4.11 decorative 圖形不計入 3:1 要求 Priority: low Discovered: 2026-04-20 — TD-006 code-review 掃同專案 raw text-{color} 時連帶發現 Location:

  • app/pages/account-pending.vue:36<UIcon text-warning size-7>
  • app/pages/admin/debug/latency/index.vue:131<UIcon text-warning size-10>
  • app/pages/admin/debug/query-logs/[id].vue:118,146<UIcon text-warning/error size-10>
  • app/pages/admin/documents/[id].vue:421<UIcon text-primary size-5>
  • app/components/chat/GuestAccessGate.vue:64<UIcon text-warning> on bg-warning/10
  • app/components/documents/LifecycleConfirmDialog.vue:103,105 — 動態 text-error / text-warning
  • app/components/documents/UploadWizard.vue:871,895,966<UIcon text-error/primary>
  • app/components/chat/CitationReplayModal.vue:121,138<UIcon text-primary>

Related markers: search @followup[TD-007] in repo

Problem

WCAG 1.4.11 Non-Text Contrast (AA) 要求傳達資訊的 icon / 圖形元件對比度 ≥ 3:1。Nuxt UI 預設 text-{color} 指向 -500 shade:

  • text-warning (#f0b100) on bg-default (white) ≈ 1.9:1
  • text-primary (#00c950) on white ≈ 1.8:1
  • text-error (#fb2c36) on white ≈ 3.3:1 — 邊緣

全部低於 3:1(或僅邊緣 pass)。TD-006 scope 只含 text,不處理 icon 對比度;e2e/td003-contrast.spec.ts 目前只掃 color-contrast rule(針對 text),未納入 non-text-contrast rule。若未來擴充 axe 掃 wcag2aaanon-text-contrast,此類會爆大量 violation。

Fix approach

與 TD-006 同策略不適用(compoundVariants 只覆蓋 component 的文字 class,不影響 icon 內的 SVG fill)。可能路徑:

  1. Icon 改用 text-{color}-700 dark:text-{color}-200:跟 TD-006 raw text 修法一樣,但 icon 視覺會變重
  2. 改用 solid chip 包住 icon<span class="rounded-full bg-warning p-2"><UIcon class="text-inverted" /></span> — full-sat bg + inverted(通常 white)text 對比 ≥ 4.5:1
  3. axe rule 維持不掃 non-text-contrast:若這些 icon 都是 decorative(aria-hidden="true"),WCAG 1.4.11 不要求(只影響 informational icon)。先 audit 每個使用點的語意,decorative 的保留,informational 的才修

建議先做 audit(選項 3 的第一步),再依結果決定選項 1 / 2。

Acceptance

  • 每處 icon 使用點明確標為 decorative(aria-hidden="true")或 informational
  • Informational icon 對比度 ≥ 3:1(可實測驗證)
  • 若擴充 e2e/td003-contrast.spec.tsnon-text-contrast rule,全 pass

TD-008 — acceptance-tc-0x MCP 整合測試在 TD-001 修後破損

Status: done Resolved: 2026-04-20 — commit 446c97d。Group B 11 個測試改用 createHubDbMock helper 一次補齊 getD1Database + getDrizzleDb 兩個 export;mcp-tool-runneractor / tokenStore injection point + createStubMcpTokenStoreFromActor helper;刪除 TD-001 後失效的 stale raw SQL assertion。pnpm test:integration:16 failed → 0 failed(51/51 files / 260 passed + 1 skipped) Priority: mid Discovered: 2026-04-20 — 跑 pnpm test:integration 為了驗證 add-ai-gateway-usage-tracking 改動時發現 16 個 acceptance-tc-_.test.ts 失敗 Location: test/integration/acceptance-tc-_.test.ts(TC-01 / 04 / 06 / 07 / 08 / 09 / 10 / 11 / 12 / 13 / 14 / 16 / 17 / 18 / 19 / 20)+ test/integration/helpers/mcp-tool-runner.ts:68Related markers: 目前無 tasks.md marker;純 pre-existing broken test,non-blocking for add-ai-gateway-usage-tracking archive

Problem

TD-001 修復後(commit 1f6a4d1,mcp-token-store 遷移 Drizzle)兩類 failure 開始發生:

  1. Error: [vitest] No "getDrizzleDb" export is defined on the "../../server/utils/database" mock — acceptance-tc-04 / 06 / 07 / 08 / 09 / 11 / 13 / 14 / 16 / 17 / 18 / 19 / 20。Drizzle 遷移後 server/utils/database.ts 新增 getDrizzleDb export,但這些 tests 的 vi.mock('../../server/utils/database', ...) 只 stub 原本的 getD1Database,沒加 getDrizzleDb
  2. TypeError: Cannot read properties of undefined (reading 'id') at mcp-tool-runner.ts:68 — acceptance-tc-01 / 10 / 12 的 runMcpCase 經由 runMcpMiddleware 解析 token,但 mock auth context 形狀對不上 Drizzle 遷移後的 shape(token record 的 id 欄位路徑改了)。

影響:

  • pnpm test:integration 非綠,擋 CI(若有)與 commit 流程的「全綠再進 archive」慣例
  • Acceptance TC 無法 local 驗證 — 本次 add-ai-gateway-usage-tracking 的 AI binding 改動是否影響 MCP read path,只能靠 unit-level mock(mcp-tool-ask.test.ts / mcp-tool-search.test.ts)推論;無 end-to-end 覆蓋

本次確認 pre-existinggit stash 我的 gateway 改動後跑 HEAD 上的 acceptance-tc-01.test.ts 仍 3/6 failed,failure log 完全一致。與 add-ai-gateway-usage-tracking 無關。

Fix approach

  1. 每個 acceptance-tc-*.test.ts 的 vi.mock('../../server/utils/database', ...)getDrizzleDb: vi.fn().mockResolvedValue(...) stub,或抽共用 helper createDatabaseMock()(類似 createHubDbMock)統一處理
  2. mcp-tool-runner.ts 的 token resolution 看當前 mcp middleware 的 token record shape,更新 mock auth context
  3. 考慮把 D1/Drizzle 的 test mock centralize 到 test/integration/helpers/database.ts(目前 createHubDbMock 只處理 hub db,沒涵蓋 getDrizzleDb 單獨 export)

Acceptance

  • pnpm test:integration 全綠(> 95% 通過,非 flake)
  • acceptance-tc-01 / 04 / 06 / 07 / 08 / 09 / 10 / 11 / 12 / 13 / 14 / 16 / 17 / 18 / 19 / 20 皆 pass
  • 未來 Drizzle schema 再變動時 mock 能跟上(helper 集中化的副產品)

TD-009 — user_profiles.email_normalized 全面改 nullable

Status: done Resolved: 2026-04-26 — passkey-user-profiles-nullable-email change archived 於 openspec/changes/archive/2026-04-26-passkey-user-profiles-nullable-email/。Migration 0016 8 表 cascade rebuild(user_profiles + 4 直接 FK children + 3 indirect chain children)+ schema.ts emailNormalized 改 nullable + auth.config.ts sentinel writer 改 null + syncUserProfile NULL branch 用 id-first lookup。實作中發現 _v16 → canonical FK pattern 在 D1 staging fail(commit bf7f423 revert),改用 _v16 → _v16 FK pattern + 依靠 D1 RENAME 自動 rewrite。已 ship v0.52.0 production deploy + smoke test 全綠。Evidence-based approval:integration spec 3/3 + unit 7/7 + wrangler D1 emulator + production smoke。 Priority: mid Discovered: 2026-04-21 — passkey-authentication change migration planning Location: server/db/schema.ts (userProfiles.emailNormalized), server/database/migrations/0009_passkey_and_display_name.sql (deferred from migration) Related markers: search @followup[TD-009] in repo

Problem

passkey-authentication change 的 design 原本規劃 user_profiles.email_normalized 同步改 nullable(落實 Decision 2 的完整語意),但 migration 0009 實務評估後延後:

  • user_profiles 的 FK children 包含 conversationsquery_logsmessagesdocuments,改 email_normalized nullable 必須 rebuild user_profiles + 其 FK 子樹(仿 0007 的 D1 cascade 模式)
  • 0009 本身已經 rebuild user 樹的 8 張表(user / account / session / member_role_changes / mcp_tokens / query_logs / citation_records / messages),再加 user_profiles 樹 rebuild 會讓 migration 超過安全 review surface(估計 700+ 行 SQL,大量 edge case)
  • 目前的 workaround:passkey-only 使用者的 email_normalized 寫入 sentinel 值 '__passkey__:' || user.id(保證 unique by PK),isAdminEmailAllowlisted 不會誤判(sentinel 含 :,不是合法 email 字元)

Fix approach

獨立 change passkey-user-profiles-nullable-email,單一職責:

  1. Migration 0010 rebuild user_profiles + FK children(conversations、query_logs、messages、documents)
  2. user_profiles.email_normalizedNULL + partial unique index(WHERE email_normalized IS NOT NULL AND email_normalized NOT LIKE '__passkey__:%'
  3. Data migration:掃 sentinel 值 → 改為 NULL
  4. 更新 server/utils/ upsert 邏輯不再寫 sentinel
  5. 更新 auth-storage-consistency spec requirement(移除 sentinel scenario,換成純 nullable scenario)

Acceptance

  • PRAGMA table_info(user_profiles) 顯示 email_normalized 允許 NULL
  • 原 passkey-only 使用者 row 的 email_normalized = NULL(sentinel 已遷移)
  • 相關查詢 code path(isAdminEmailAllowlisted 等)皆加 email_normalized IS NOT NULL guard
  • PRAGMA foreign_key_check 零 row
  • spectra analyzepasskey-authentication archived spec 的 nullable rule 生效

TD-010 — credentials / admin-members endpoint libsql 不相容

Status: done Resolved: 2026-04-23 — portable ORM refactor、local happy-path 響應式驗證與 production /account/settings + /admin/members manual regression evidence 全數補齊,drizzle-refactor-credentials-admin-members closeout 條件滿足 Priority: mid Discovered: 2026-04-21 — passkey-authentication §16 Design Review 跑 /review-screenshot 時,/account/settings/admin/members 兩頁回 500 Location:

  • server/api/auth/me/credentials.get.tsdb.all(sql\SELECT ... COALESCE(display_name, "displayName", name) ...`)`)
  • server/api/admin/members/index.get.ts:127-164db.all(sql\... EXISTS (SELECT 1 FROM account) ...`)`)

Related markers: search @followup[TD-010] in repo

Progress update (2026-04-21):

  • Local dev http://localhost:3010/api/_dev/login 建立 admin@test.local session 後重新驗證 TD-010 happy path。
  • /api/auth/me/credentials 回 200:email = "admin@test.local", displayName = "Test Admin", hasGoogle = false, passkeys = []
  • /api/admin/members?page=1&pageSize=20 回 200,data.length = 17,列資料包含 displayName / credentialTypes / registeredAt / lastActivityAt
  • Playwright 載入 /account/settings/admin/members 皆 HTTP 200、停留在目標 URL、未偵測已知 error text;截圖在 screenshots/local/td010-continuation/
  • 2026-04-23 使用者於 production admin session 手動驗證:/account/settings happy path 正常顯示 email / display name / passkey / Google 綁定區塊;/admin/members happy path 正常顯示會員列表 / role badge / credential badges / last activity,且本次資料量全部落於單頁,未出現 500 / 暫時無法載入會員清單 / error state。
  • local UI、production D1 回歸與 §16 responsive pipeline 已全數補齊,Status 更新為 done

Problem

兩個 endpoint 使用 db.all(sql\...`)raw SQL + tagged template(drizzle 的 D1-specific API),在 production D1 正常運作,但在 local dev 的 libsql 環境下db.all` 不存在/行為不同,導致 endpoint 500。同類型問題見 TD-001(已修)。

影響範圍:

  • /account/settings 頁面無法在 local 渲染 happy path(永遠 error state)
  • /admin/members 列表無法在 local 渲染 happy path(永遠 error state)
  • §16 Design Review 響應式截圖 6/12 只能拍到 error state;happy path 留待 §17 人工檢查(在 production/或修完 TD-010 的 local)驗證
  • admin-members-list.spec.tsadmin-members-passkey-columns.spec.ts 這類 integration test 若依賴 local libsql 會 mock/skip,production 側才真正驗證

Fix approach

仿 TD-001 做法,把 raw SQL 改寫為 Drizzle ORM query:

  1. credentials.get.ts
    • SELECT email, display_name, hasGoogle, passkeys[] 拆成 3 條 drizzle query(user / account filter providerId='google' / passkey by userId)
    • 取消 COALESCE(display_name, "displayName", name) — FD-001 既已改以 fieldName: 'display_name' 對齊 schema,drizzle schema.user.displayName 直接讀到 snake_case 值
  2. admin/members/index.get.ts
    • EXISTS (SELECT 1 FROM account WHERE providerId = 'google' ...) → drizzle leftJoin + groupBy 或 subquery(drizzle-orm 支援 sql\EXISTS(...)`` inline but 需保 libsql 相容寫法)
    • credentialTypes 聚合改以 application-layer 組裝(查 account 後 reduce)
    • registeredAt / lastActivityAt drizzle query 直接可得(user.createdAtsession.createdAt max)

Acceptance

  • Local dev 環境(hub:db sqlite)執行 curl /api/auth/me/credentials with 有 session 的 cookie → 200 with correct payload
  • Local dev 執行 curl /api/admin/members with admin cookie → 200 with correct payload
  • 再次跑 /review-screenshot 應可拍到 happy path 的響應式佈局
  • production D1 側回歸:admin-members-list.spec.ts + admin-members-passkey-columns.spec.ts 全綠

TD-011 — migration 0009 FK cascade 設計不符 self-delete / audit 語意

Status: done Resolved: 2026-04-23 — migration 0010_fk_cascade_repair.sql 已套用至 production D1,並以 v0.28.12 完成 passkey-only self-delete production closeout Priority: high Discovered: 2026-04-21 — passkey-authentication §17.8 passkey-only 自刪實測,/api/auth/account/delete 回 500 Failed query,sqlite FK 阻擋 user row 刪除 Location: server/database/migrations/0009_passkey_and_display_name.sql

  • member_role_changes (line ~296-304): FOREIGN KEY (user_id) REFERENCES user_new(id) 無 ON DELETE 子句 → 預設 NO ACTION → 阻擋 user row 刪除
  • mcp_tokens (line ~183): created_by_user_id TEXT NOT NULL REFERENCES user_new(id) 同樣無 ON DELETE → 阻擋刪除

Related markers: search @followup[TD-011] in repo

Progress update (2026-04-21):

  • server/database/migrations/0010_fk_cascade_repair.sql 已套用到 production D1 agentic-rag-db(database_id 3036df7f-d54b-4d36-a33d-ecbb551fc278)。
  • Pre-apply backup 已下載:backups/backup-pre-0010-20260421.sql
  • Production baseline / post-apply row count 一致:member_role_changes=2, mcp_tokens=3, query_logs=72, citation_records=37, messages=81, "user"=2
  • Post-apply PRAGMA 驗證通過:foreign_key_check empty;member_role_changes 無 FK;mcp_tokens.created_by_user_idON DELETE CASCADEquery_logs.mcp_token_idON DELETE SET NULL
  • Local WebAuthn 自刪驗證通過:Playwright virtual authenticator 建立 passkey-first user td011-mo8ftwv1,插入 local mcp_tokens row 後完成 /account/settings 刪除流程;POST /api/auth/account/delete 回 200、導回 /member_role_changes.reason = 'self-deletion' tombstone 保留、該 user 的 token count 回 0;截圖 screenshots/local/td011-self-delete-local.png
  • Local .data/db/sqlite.db compatibility DB 也已修正 query_logs / citation_records / messages FK rebind,query_logs.mcp_token_id 指向 canonical mcp_tokens(id) ON DELETE SET NULL
  • 2026-04-23 production closeout 已完成:v0.28.12 重新實測 passkey-only test user 自刪,generate-authenticate-options / verify-authentication / /api/auth/account/delete / /api/auth/sign-out 全部回 200;最終 hard redirect 回 //api/auth/get-sessionnull,首頁恢復登入文案。
  • 同輪 production D1 驗證:member_role_changes latest row reason = 'self-deletion'"user" / passkey / mcp_tokens 對該 test user 的 count 全為 0
  • TD-011 已完成收尾,Status 改為 done,保留條目供後續追溯。

Problem

task 7.2 設計意圖:

member_role_changes 寫入 reason = 'self-deletion' 後保留為 tombstone

但實際 migration 0009 給 member_role_changes.user_id 加上 FK 且沒有 ON DELETE 子句,SQLite 預設 NO ACTION = RESTRICT → 當存在 audit row 時,delete user row 被 DB 層阻擋。tombstone 完全無法寫入。

mcp_tokens.created_by_user_id 類似問題,雖然語意該是「user 刪除 → token 也失效(cascade)」,但 migration 也沒寫 ON DELETE CASCADE。

影響:

  • Passkey-only user 自刪(§17.8 人工檢查)在 production + local 皆 500
  • Audit tombstone 機制完全無效(passkey-authentication 的合規承諾 broken)
  • Admin 用 /api/admin/members/:userId 刪除使用者也會撞同一顆石頭

本 session 曾套用 local-only 修正(直接 rebuild 兩個表);後續已由 migration 0010 正規化並套用到 production D1。

Fix approach

新 migration 0010_fk_cascade_repair.sql(範圍於 2026-04-21 spectra-apply 兩次擴大:先擴成 5 表 rebuild 鏈,再加 query_logs 語意變更):

  1. member_role_changes:rebuild 移除 FK constraint(audit tombstone 需要在 user 刪除後仍存活,所以 user_id 只是純 text reference,不設 FK)。index idx_member_role_changes_user_created 保留。
  2. mcp_tokens:rebuild 把 created_by_user_id 改為 REFERENCES "user"(id) ON DELETE CASCADE,讓 token 隨 user 刪除自動清除。
  3. query_logs:rebuild 把 mcp_token_id 改為 REFERENCES "mcp_tokens"(id) ON DELETE SET NULL(若保持預設 NO ACTION 則 user → mcp_tokens CASCADE 會被此 FK RESTRICT,TD-011 的 bug 只會往下移動一層;TDD red 測試已證實)。observability log 保留,token 歸屬在 token 已刪除後 NULL 化。
  4. citation_records / messages:連帶 FK re-bind 到新 query_logs,columns 與 ON DELETE 子句完全保持 0009 狀態。
  5. 走 0007 / 0008 / 0009 的 rebuild 模式(PRAGMA defer_foreign_keys = ON*_new + INSERT SELECT → children-first DROPRENAME)。children-first DROP(messages → citation_records → query_logs → mcp_tokens)避免 messages.query_log_id ON DELETE SET NULL 在 DROP query_logs 時靜默觸發。
  6. Release checklist:在 production D1 apply 前確認備份 + 五張 rebuild 表 row count 對照。

Acceptance

  • PRAGMA foreign_key_checkmember_role_changes / mcp_tokens / query_logs / citation_records / messages 回 empty
  • PRAGMA foreign_key_list(query_logs) 顯示 mcp_token_idon_delete = 'SET NULL'
  • DELETE FROM "user" WHERE id = '<passkey-only-test-user>' 成功(由 /api/auth/account/delete 觸發),audit row 保留,相關 token CASCADE 清除
  • user 刪除後其 query_logs 仍存在,mcp_token_id 為 NULL,query_redacted_text / created_at / channel / environment 不變
  • §17.8 人工檢查 local + production 皆通過
  • test/integration/passkey-self-delete.spec.ts 新增 test case 覆蓋 audit tombstone 存在時能成功刪除 user,以及 query_logs 在 token cascade 後保留且 mcp_token_id = NULL

Status: done Resolved: 2026-04-23 — 透過 passkey-first-link-google-custom-endpoint change 落地 custom endpoint pair,完成 local / production 人工驗證、allowlist 升權驗證與 archive Priority: high Discovered: 2026-04-21 — passkey-authentication §17.3 實機測試 passkey-first 帳號點 /account/settings 的「綁定 Google 帳號」,/api/auth/link-social 回 200 但 OAuth callback 回 please_restart_the_process,後端 log 顯示 Failed to parse state: link.email expected string, received null

Location:

  • app/pages/account/settings.vue handleLinkGoogle call path
  • better-auth core parseGenericState / link-social endpoint(node_modules/better-auth/dist/api/routes/account.mjs 約 line 148)要求 session.user.email 非空

Related markers: search @followup[TD-012] in repo

Problem

better-auth linkSocial endpoint 在建構 OAuth state 時,把 session.user.email 塞進 link.email 欄位並用 Zod 驗證必須是 string。passkey-first 帳號(email = NULL)直接通不過 state parse → OAuth callback 拒絕。

這是 better-auth 設計層的限制(intent 是用 email 比對防 account takeover),無法透過 allowDifferentEmails: true 之類 config 繞過;config 在 parse 之後才生效。

影響:passkey-authentication 的 Decision 5 / §17.3 scenario「passkey-first 使用者綁 Google」無法透過 better-auth 原生 API 實作

Fix approach

新增 custom endpoint pair(繞開 better-auth linkSocial,自建 OAuth flow):

  1. GET /api/auth/account/link-google-for-passkey-first
    • requireUserSession + 驗 session.user.email === null
    • 建 OAuth state(自己的 cookie / KV key,帶 session.user.id)
    • redirect 到 Google authorization URL(用現有 NUXT_OAUTH_GOOGLE_CLIENT_ID 與 redirect_uri)
  2. GET /api/auth/account/link-google-for-passkey-first/callback
    • 收 Google code + 自家 state
    • 用 code 換 access token + id_token(直接 fetch Google token endpoint)
    • 解 id_token 取 email / name / image
    • 檢查 email 是否已在其他 user.id 上使用 → 若 yes 回 EMAIL_ALREADY_LINKED 409
    • UPDATE "user" SET email = <google-email>, image = <google-image> WHERE id = <session.user.id>
    • INSERT INTO account (userId, providerId='google', accountId, accessToken, idToken, refreshToken, scope, createdAt, updatedAt) VALUES (...) 跟 better-auth schema 對齊
    • databaseHooks.session.create.before 下次會自動走 reconciliation(跑 allowlist 比對 → 升 admin 若符合)
    • redirect 回 /account/settings?linked=google
  3. app/pages/account/settings.vuehandleLinkGoogle 改指向新 endpoint(僅當 credentials.email === null;否則仍用 better-auth linkSocial)

Acceptance

  • passkey-first 使用者(email=NULL)點「綁定 Google 帳號」→ OAuth 走通 → email 填入 + Google account row 建立 + passkey row 保留 → 下次登入可用 passkey 或 Google 任一
  • 衝突處理:若 Google email 已屬另一 user.id → 409 UX 顯示清楚
  • Allowlist reconciliation:若綁的 Google email 在 ADMIN_EMAIL_ALLOWLIST → 下次 session refresh 自動升 admin(既有 session.create.before 機制)
  • test/integration/passkey-first-link-google.spec.ts 覆蓋 happy path + 衝突 409 + allowlist upgrade

TD-013 — /account/settings 新增 passkey 缺 naming dialog

Status: done Resolved: 2026-04-21 — app/pages/account/settings.vue 新增 nameDialogOpen / passkeyNameInput state + UModal(輸入 passkey 名稱 + 驗證 + 傳給 client.passkey.addPasskey({ name })Priority: low Discovered: 2026-04-21 — passkey-authentication §17.2 實機驗證 Google-first 加綁 passkey,列表顯示「未命名 passkey」 Location: app/pages/account/settings.vue handleAddPasskeyRelated markers: search @followup[TD-013] in repo

Problem

handleAddPasskey 直接呼叫 client.passkey.addPasskey() 沒有傳 namepasskey.name 欄位留空 → 列表顯示「未命名 passkey」,多個裝置時難以辨識(尤其 revoke 誤刪風險)。

Fix applied

加一個 naming dialog:點「新增 Passkey」不直接啟動 ceremony,先開 modal 讓使用者輸入名稱(maxlength 40,必填),確認後帶 name 呼叫 addPasskey。驗證失敗或空字串在 modal 內直接顯示 inline error。


TD-014 — error-sanitizer 後 12 test 拋 evlog Logger not init

Status: done Resolved: 2026-04-24 — 本地重跑 pnpm test:integration 已恢復全綠(72 files / 364 tests passed / 1 skipped),不再重現 evlog Logger not initializedPriority: mid Discovered: 2026-04-21 — drizzle-refactor-credentials-admin-members apply 階段跑 pnpm test:integration 發現,pre-existing 非本次引入 Location: 影響的 test 檔:

  • test/integration/acceptance-tc-ui-state.test.ts(5 個 sub-test)
  • test/integration/admin-documents-route.test.ts(3 個)
  • test/integration/dev-login-route.test.ts(2 個)
  • test/integration/publish-route.test.ts(2 個)

Related markers: search @followup[TD-014] in repo

Problem

v0.25.0 commit df49b11 引入「全站 server API 錯誤訊息洩漏防護」的 error-sanitizer nitro plugin。該 plugin 改動了 evlog logger 的初始化時序,造成上述 12 個 integration test 在呼叫 handler 時拋出:

[evlog] Logger not initialized. Make sure the evlog Nitro plugin is registered. If using Nuxt, add "evlog" to your modules.

影響:

  • pnpm test:integration full run 現在有 12 個紅燈(但全是 pre-existing / 非 TD-010 引入)
  • CI 如果有 test gate 會被擋;production D1 端 runtime 看起來 OK(handler 仍正常執行,只是 test stub 少了 plugin)

本 TD 不在 TD-010 scope(TD-010 是 refactor drizzle query builder,與 evlog plugin 時序無關),但 apply 階段發現需登記。

Resolution note

2026-04-24 重新執行 pnpm test:integration,受影響 integration suite 已不再出現 logger 初始化錯誤;目前 repo 內也沒有殘留 @followup[TD-014] marker。此條保留於 tech debt register 僅供追溯,不再列為 open item。

Acceptance

  • pnpm test:integration 全綠(除非後續有其他新的 real regression)
  • 不得以 .skip 繞過
  • TD-014 marker 在修復 PR 的 tasks 標註 @followup[TD-014]

TD-015 — SSE 長連線缺 heartbeat,30s proxy timeout 風險

Status: done Resolved: 2026-04-26 — v0.49.0 add-sse-resilience 已 archive;v0.50.1 production 立即採樣(取代原 7 天觀察期):過去 7 天 production 共 75 條 query_logs(v0.49.0 deploy 後 v0.49.0+ 樣本:含人工觸發共 6 條 chat — 1 條 no_citation + 1 條 pipeline_error + 1 條 accepted 成功 + 1 條 no_citation markdown + 2 條 pipeline_error)。first_token_latency_ms 兩條健康樣本 = 7254 ms / 3245 ms(量測有效、非 0、非異常)。refusal_reason 分布皆屬業務正常 refusal 或 TD-056 已知 case;live wrangler tail 採樣 5 分鐘無 connection closed / NetworkError / 30s+ 阻塞訊號,acceptance 條件「chat.error 計數無顯著上升」滿足。Manual production URL 30s+ reasoning 驗收與長期 latency 抽查留作 backlog(不阻擋 done)。Live tail 順帶證實 TD-057 warning 在 production 真實重現(log.error × 3 + log.set × 3,全在 SSE 路徑),詳見 TD-057 entry。 Priority: mid Discovered: 2026-04-24 — /commit code-review(web-chat SSE streaming) Location: server/api/chat.post.ts:createSseChatResponseRelated markers: search @followup[TD-015] in repo

Problem

Cloudflare Workers SSE 經過 CF edge / 某些瀏覽器代理時,若長時間(~30s)沒資料會被主動關閉。createSseChatResponse 在發出 ready 後、收到 Workers AI 首 token delta 前,若生成延遲 > 30s,client 會看不到任何後續事件直接掉線。

Fix approach

ReadableStream.start 內啟動一條 keep-alive 迴圈,每 15-20 秒 enqueue 一個 SSE 註解行(: keep-alive\n\n),直到正常終止或 abort。需注意:

  • 使用 AbortController 讓 cancel 時能一起收尾
  • 迴圈 enqueue 需檢查 closed 旗標,避免 race
  • 時間間隔須顯著小於 CF 的 idle threshold

Acceptance

  • [x] Chat SSE 連線在 Workers AI 首 token 延遲 ≥ 30s 時 client 仍持續收到事件流,不掉線(code 已 land — add-sse-resilience change,archive 後 production deploy 驗)
  • [x] 新增 unit test 模擬 slow first token,assert heartbeat block 有被送出(test/unit/chat-route-heartbeat.spec.ts,3 cases:emit / terminate stop / consumer cancel)
  • [ ] Manual QA 在 production 環境觀察 30s+ 延遲的 chat 不再觸發 NetworkError / connection closed留作 backlog,不阻擋 done;建議:本機開 production URL 發一條會觸發長 reasoning 的 query 即時驗證
  • [x] 立即採樣 production logs(取代原「7 天觀察」):2026-04-26 跑 wrangler d1 execute agentic-rag-db --remote 撈 7 天 query_logs 統計 — accepted: 71 / blocked: 4,無 SSE heartbeat 引入的新 error 模式;refusal 分布皆屬業務正常或已知 TD-056。acceptance 條件滿足
  • [x] 立即抽 production D1 最近樣本 first_token_latency_ms:v0.49.0 deploy 後唯一成功樣本 7254 ms(量測有效、非 0、非異常),確認 first-token 量測未被 : keep-alive 行誤計(schema 改用 first_token_latency_ms 直接欄位、不需手動算 delta)

TD-016 — isAbortError / createAbortError 在四處重複實作

Status: done Resolved: 2026-04-25 — 抽到 shared/utils/abort.ts(export isAbortError + createAbortError,跨 browser / Workers runtime 適用),4 個 caller (app/utils/chat-stream.ts / server/api/chat.post.ts / server/utils/web-chat.ts / server/utils/workers-ai.ts) 改 import { ... } from '#shared/utils/abort' 並刪 local 實作。新增 test/unit/abort.spec.ts 12 cases 覆蓋 DOMException + duck typing + null/undefined/string/number negative + round-trip。 Priority: low Discovered: 2026-04-24 — /commit simplify review Location:

  • app/utils/chat-stream.ts
  • server/api/chat.post.ts
  • server/utils/workers-ai.ts
  • server/utils/web-chat.ts

Related markers: search @followup[TD-016] in repo

Problem

同一對 helper 在四個檔案各自實作,行為一致(檢查 DOMException.name === 'AbortError' / 建立 new DOMException('aborted', 'AbortError'))。若未來改語意(例如加 reason)需要四處同步。

Fix approach

抽共用到 shared/utils/abort.ts,四處 import。注意 shared 層需同時可用於 app(browser)與 server(Workers runtime),DOMException 在兩邊皆有。

Acceptance

  • 四處改為 import { isAbortError, createAbortError } from '#shared/utils/abort'
  • 原本的 local function 刪除
  • 既有 unit / integration test 持續綠

TD-017 — chat.post.ts 兩個 AI binding getter 可合併

Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: low Discovered: 2026-04-24 — /commit simplify review Location: server/api/chat.post.ts:getRequiredAiSearchBinding / getRequiredWorkersAiBindingRelated markers: search @followup[TD-017] in repo

Problem

兩個 getter 都讀 getCloudflareEnv(event).AI、檢查某個 method 存在、拋 503。重複 skeleton。

Fix approach

抽共用:

ts
function requireAiBinding<T>(event: H3Event, input: { method: keyof T; message: string }): T {
  const binding = getCloudflareEnv(event).AI
  if (
    !binding ||
    typeof (binding as Record<string, unknown>)[input.method as string] !== 'function'
  ) {
    throw createError({
      statusCode: 503,
      statusMessage: 'Service Unavailable',
      message: input.message,
    })
  }
  return binding as T
}

或兩個 getter 共享一次 getCloudflareEnv(event).AI 讀取。

Acceptance

  • getRequiredAiSearchBinding / getRequiredWorkersAiBinding 改為薄 wrapper
  • chat.post.ts 無行為變更,pnpm test:integration

TD-018 — Container.vue classifyError 巢狀條件抽 lookup table

Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: low Discovered: 2026-04-24 — /commit simplify review Location: app/components/chat/Container.vue:classifyErrorRelated markers: search @followup[TD-018] in repo

Problem

HTTP status code → error kind 的 mapping 目前用三元鏈 + 巢狀 if,第 3 層接近警戒線。

Fix approach

readErrorStatus(error): number | undefined + STATUS_TO_KIND: Record<number, ErrorKind>,主邏輯 flatten。

Acceptance

  • classifyError 扁平,單層邏輯
  • 既有 chat-container.spec.ts 持續綠

TD-019 — SSE reader pattern 在 client/server 雷同可抽共用

Status: done Resolved: 2026-04-26 — add-sse-resilience change archive。新增 shared/utils/sse-parser.ts(export readSseStream + ReadSseStreamInput + SseBlock),統一處理 reader / decoder / buffer.split('\n\n') 主迴圈、abort race handling(signal.addEventListener('abort', () => reader.cancel(createAbortError())) 配合 main loop signal.aborted 檢查 + finally 清 listener + releaseLock)、comment block detection(: keep-alive 等註解 block 不轉發給 onBlock)。app/utils/chat-stream.ts:readChatStreamserver/utils/workers-ai.ts:readStreamedTextResponse 改用 readSseStream,block handler 各自處理 chat event type / [DONE] sentinel + JSON delta,外部簽章與 emit 行為不變。新增 test/unit/sse-parser.spec.ts 10 cases 覆蓋 normal / multi-block / partial trailing / abort mid-stream / signal pre-aborted / UTF-8 邊界 / comment block / terminate / missing body;既有 chat-stream / workers-ai / chat-route integration spec 全綠。Land 在 v0.48.0 commit cd43bf2,archive 於 v0.49.0 後。 Priority: low Discovered: 2026-04-24 — /commit code-review Location: app/utils/chat-stream.ts:readChatStream + server/utils/workers-ai.ts:readStreamedTextResponseRelated markers: search @followup[TD-019] in repo

Problem

兩處都:reader.read() → decoder.decode → split('\n\n') → buffer.pop() → parseSseBlock,abort handler / finally / releaseLock 幾乎 1:1 雷同。目前分別維護,有漂移風險(例如一邊修 bug 另一邊漏改)。

Fix approach

shared/utils/sse-parser.ts

  • 公共 readSseStream(response, { onBlock, signal }) — 處理 reader / decoder / block 切分 / abort
  • 兩邊 caller 只需提供 block handler

注意:server 端的 block handler 需認識 [DONE];client 端需認識 event-type 解析。可透過 callback 傳入。

Acceptance

  • [x] app/utils/chat-stream.tsserver/utils/workers-ai.ts 共用同一個 SSE reader
  • [x] 既有 unit / integration test 持續綠
  • [x] 新增或擴充 sse-parser 的 unit test

TD-020 — CHATGPT_CONNECTOR_OAUTH_PATH_PATTERN 可收緊字元集

Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: low Discovered: 2026-04-24 — /commit code-review Location: server/utils/mcp-chatgpt-registration.ts:CHATGPT_CONNECTOR_OAUTH_PATH_PATTERNRelated markers: search @followup[TD-020] in repo

Problem

現行 regex /^\/connector\/oauth\/[^/?#]+$/ 允許任意字元(Unicode、. 等)。雖然 origin 已限定 https://chatgpt.com,但 segment 字元集可以更嚴。

Fix approach

改成 /^\/connector\/oauth\/[A-Za-z0-9_-]{1,64}$/,並確認 OpenAI Connector OAuth ID 實際允許的字元集(避免誤擋合法 segment)。

Acceptance

  • isAllowedChatGptConnectorRedirectUri 拒絕含 . / Unicode / 超長 segment 的 URI
  • 既有 unit test 仍綠;新增 case 涵蓋新限制

TD-021 — ConversationHistory bucket toggle 缺 aria-expanded;onExpandRequest 應轉 emit

Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: low Discovered: 2026-04-24 — /commit code-review(collapsible-chat-history-sidebar archive) Location: app/components/chat/ConversationHistory.vue(bucket toggle button、onExpandRequest prop) Related markers: search @followup[TD-021] in repo

Problem

  1. bucket toggle <button> 沒有 aria-expanded / aria-controls;目前靠 Nuxt UI UCollapsible:open 外控。e2e axe 已過,但 toggle 本身未對 AT 明示狀態變化
  2. onExpandRequest?: () => void 以 callback-prop 形式宣告,但同檔已用 defineEmits 管理 conversation-cleared / conversation-selected,event 契約分裂在 props + emits 兩處

Fix approach

  • 在 toggle 按鈕補 :aria-expanded="bucketOpenState[group.bucket]"
  • onExpandRequest 改為 'expand-request': [] emit,父層改綁 @expand-request="expandHistorySidebar"

Acceptance

  • 新增 axe / a11y 單元測試驗證 bucket toggle 的 aria-expanded 依狀態更新
  • defineEmits 宣告包含 expand-requestindex.vue 改用 @expand-request,e2e 仍綠

TD-022 — groupedConversations 不跨 midnight 重新分組

Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: low Discovered: 2026-04-24 — /commit code-review Location: app/components/chat/ConversationHistory.vue:groupedConversations computed Related markers: search @followup[TD-022] in repo

Problem

groupedConversations computed 只在 conversations 變動時 re-run,new Date() 在掛載當下被捕捉。若頁面長開跨過午夜,原本分到「今天」的對話不會自動移到「昨天」,需要 refetch 才會更新。

Fix approach

引入一個「當前時間」tick(如 useNow({ interval: 60_000 }) 或跨午夜的 one-shot timer),讓分組在日期切換時重新計算;或在 visibility change / refetch 觸發時強制重新分桶。

Acceptance

  • 跨午夜後無需 refetch,時間桶自動重分類(有單元測試以假時鐘覆蓋)
  • 不引入每秒重 render

TD-023 — index.vue 雙 LazyChatConversationHistory 產生重複 /api/conversations fetch

Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: low Discovered: 2026-04-24 — /commit code-review(既存 pattern) Location: app/pages/index.vue(inline lg sidebar + drawer 分支各自掛 LazyChatConversationHistoryRelated markers: search @followup[TD-023] in repo

Problem

<lg 的 drawer 與 lg 的 inline sidebar 各自 mount 一個 LazyChatConversationHistoryuseChatConversationHistory 在每個 instance 以 immediate: true 觸發 /api/conversations,造成登入首次渲染時出現兩次並行 fetch。

Fix approach

useChatConversationHistory hoist 到 index.vue,將 state 以 props 傳給兩個 surface;或讓 drawer 以 v-if="historyDrawer.isOpen.value" 延後掛載,避免同時 mount。

Acceptance

  • 首頁首次渲染只觸發一次 /api/conversations GET(Network 驗證 + e2e assert)
  • 兩個 surface 仍顯示同一來源資料、互不衝突

TD-024 — chat-history-sidebar 測試品質:string contract + Playwright resolves

Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: low Discovered: 2026-04-24 — /commit code-review(testing anti-patterns) Location:

  • test/unit/chat-history-sidebar-source-contract.test.ts
  • e2e/collapsible-chat-history-sidebar.spec.ts(約 L216-218)

Related markers: search @followup[TD-024] in repo

Problem

  1. chat-history-sidebar-source-contract.test.ts 全篇以 readFileSync + toContain 比對 .vue raw source(class 片段、icon 名、aria-label 文字),任何無害重構(如 class 重排、把 icon 抽常數)會被誤判為違規,違反 testing-anti-patterns.md 的「test behavior, not source strings」
  2. e2e spec 使用 await expect(page.evaluate(...)).resolves.toBe('true')@playwright/testexpect 沒有 .resolves matcher,可能 silently pass 而未真的 assert

Fix approach

  • 把 source-contract test 轉為 mountSuspended 元件測試(驗 DOM / aria / storage key 行為),或直接移除並信賴 e2e 覆蓋
  • e2e spec 改為 expect(await page.evaluate(...)).toBe('true') 形式

Acceptance

  • 移除 / 改寫後 pnpm test + pnpm test:e2e 仍綠
  • 重構 .vue 不再需要改 contract test
  • Playwright expect 對 evaluate 結果真正 assert(人工故意打壞邏輯可看到 red)

TD-025 — Container.vue $csrfFetch.native 跳過 CSRF header 造成 /api/chat 403

Status: done Resolved: 2026-04-24 — code-quality-review-followups Priority: high Discovered: 2026-04-24 — code-quality-review-followups 人工檢查 10.x 送不出 chat 訊息 Location: app/components/chat/Container.vue:193Related markers: 無(直接在 scope 內修)

Problem

2026-04-24 凌晨的 SSE streaming refactor(commit c6ea971)把 $csrfFetch('/api/chat', ...) 改成 $csrfFetch.native(...).native 是 ofetch 繼承自 globalThis.fetch 的 raw fetch,經過 nuxt-csurf 的 onRequest hook,所以 x-csrf-token header 根本沒被加,server csurf 驗證固定 403 CSRF Token Mismatch。任何登入使用者送出 chat 訊息就會打到這個錯。

單元 / 整合測試都 mock 掉 fetch 或直接呼叫 server handler,不會 trigger .native 的真實呼叫路徑。所以 test 全綠、production 炸。

Fix approach

保留 .native(streaming 需要 raw Response + readable body)但手動用 useCsrf() 取 token 塞進 headers:

ts
const { csrf, headerName } = useCsrf()
const headers: Record<string, string> = {
  accept: 'text/event-stream',
  'content-type': 'application/json',
}
if (csrf && headerName) headers[headerName] = csrf
const response = await $csrfFetch.native('/api/chat', { method: 'POST', body, headers, signal })

Acceptance

  • 本地登入後送訊息能收到 streaming token / terminal event
  • Network tab 看到 POST /api/chat request headers 有對應 csrf header
  • 既有單元 / 整合 test 仍綠

TD-026 — index.vue 與 ConversationHistory fallback 重複 config + refresh 邏輯

Status: done Resolved: 2026-04-25 — consolidate-conversation-history-config change 抽出 createChatConversationHistory factory 集中管理兩處 config 與 refresh reconcile 順序。 Priority: low Discovered: 2026-04-24 — code-quality-review-followups /commit 0-A simplify review Location: app/pages/index.vue (~L44-60) + app/components/chat/ConversationHistory.vue (~L40-60) Related markers: search @followup[TD-026] in repo(archived change code-quality-review-followups 保留歷史 marker)

Resolution

app/composables/create-chat-conversation-history.ts 封裝 useChatConversationHistory config 組裝與 refreshAndReconcile(selectedId) 實作;app/pages/index.vueapp/components/chat/ConversationHistory.vue 兩處分別以 parent-provide 與 owner-fallback 方式呼叫同一 factory,toast / callback 文案集中維護。新增 test/unit/create-chat-conversation-history.spec.ts 覆蓋 refresh reconcile 四條路徑(still present / missing but loadable / missing + detail missing / no active id)+ 預設 toast fallback 兩條測試;spec web-chat-ui 新增「Conversation History Refresh Reconciliation」requirement 將 reconcile 順序正式化。

Problem

TD-023 引入 provide/inject 後,ConversationHistory.vue 保留 owner-fallback 分支(用於 test 或無 parent provide 的場景)。這條 fallback 分支把 parent index.vue 裡的 useChatConversationHistory({...}) config(listConversations / deleteConversation / loadConversation / 4 個 toast handler 回呼)與 refreshConversationHistory() 的 body(refresh → 查存在性 → detail fallback → cleared notification)幾乎逐行複製兩份

  • parent 和 owner-fallback 會在未來同步維護時漂移(如 toast 文案換、callback signature 改)
  • Test 目前都透過 vi.mock 攔截 useChatConversationHistory,所以兩處的差異不會被 test 抓到
  • 語義上 parent 永遠會 provide,owner-fallback 只為 test convenience,實際無使用者路徑會真的走 fallback

Fix approach

抽成 createChatConversationHistory($csrfFetch, toast, { onConversationSelected, onConversationCleared, selectedConversationId }) factory(放 app/composables/useChatConversationHistory.ts 或相鄰 utils):

  • 回傳 { api, refreshAndReconcile(selectedId?) }
  • parent index.vue 與 ConversationHistory owner-fallback 都呼叫同一個 factory
  • Toast / callback config 集中維護

或更激進:移除 owner-fallback 路徑,test 改用 createTestingPinia 之類 provide 真 instance 到 mount root,讓 production 與 test 走同一條路。

Acceptance

  • index.vueConversationHistory.vue 沒有重複的 useChatConversationHistory config literal
  • 既有單元測試(conversation-history-{aria,midnight,component}.spec.ts)仍綠
  • e2e chat-home-fetch-dedup.spec.ts 仍綠
  • Factory 有至少一個直接的 unit test 覆蓋 refresh reconcile 行為

TD-027 — MCP connector first-time authorization journey 實測待部署後驗證

Status: in-progress — local backend migration verified; staging deploy/smoke/D1/MCP evidence pending authorization. Priority: mid Discovered: 2026-04-24 — auth-redirect-refactor 人工檢查 7.4 Location: app/pages/auth/mcp/authorize.vueapp/utils/mcp-connector-return-to.tsapp/pages/auth/callback.vueRelated markers: search @followup[TD-027] in repo

Problem

auth-redirect-refactor 改動:

  1. /auth/mcp/authorize 的 Google login handler 加 callbackURL: '/auth/callback'(避免 better-auth 預設回 /
  2. /auth/callback consume order 改為 MCP > generic > fallback /

以上改動需要透過 Claude.ai 實際發起 MCP connector connection 才能 end-to-end 驗證,但目前 local dev 無法被 claude.ai 直接連到(需 ngrok / cloudflare tunnel / 部署到 staging)。人工檢查 7.4 因此暫未驗證。

Fix approach

部署到 staging 或 production 後,執行人工驗收流程:

  1. Claude.ai MCP connector 指向已部署的 MCP endpoint
  2. 發起連接 → 被導去 https://<deployed-host>/auth/mcp/authorize?client_id=...&redirect_uri=...&...
  3. 點 Google 登入 → OAuth 完成
  4. 必須回到原 /auth/mcp/authorize?... 同樣 URL(驗 saveMcpConnectorReturnTo sessionStorage bridge)
  5. 看到授權同意畫面 → 點授權 → 回 Claude.ai
  6. 在 Claude.ai 能正常使用 MCP tools

Acceptance

  • Staging / production 完成上述 6 步流程無中斷、無錯誤
  • 步驟 4 的 URL 是原始 authorize URL 含 query,而非 / / /auth/login
  • Claude.ai 端 connector 狀態顯示 connected 且可呼叫工具
  • 完成後將 7.4 marker 從 tasks.md 移除並更新 TD-027 Status 為 done

TD-028 — DeleteAccountDialog Google reauth 無 callbackURL,dialog 會 unmount

Status: done Resolved: 2026-04-25 — fix-delete-account-dialog-google-reauth 已 archive;使用者完成 Google reauth delete flow 人工驗證,3010 本機補充驗證也覆蓋 direct ?open-delete=1 不繞過、dialog reauth gate、pending-delete resume confirm step。 Priority: mid Discovered: 2026-04-24 — auth-redirect-refactor code-review OBS-1 Location: app/components/auth/DeleteAccountDialog.vue handleGoogleReauthRelated markers: 尚無 tasks.md marker(pre-existing,非本 change scope)

Problem

handleGoogleReauth 呼叫 signIn.social({ provider: 'google' }) 未指定 callbackURL,better-auth 預設回 /。使用者流程:

  1. /account/settings → 按「刪除帳號」→ dialog 開啟 → 按「Google 重新驗證」
  2. 跳 Google OAuth → 回到本站 /
  3. Dialog 已 unmountreauthComplete = true 設在 unmounted instance
  4. 使用者看到 / 首頁,沒有任何指示「session 已 rotate」
  5. 必須從頭再開一次 delete 流程

Passkey reauth 是同 origin 不受影響。

Fix approach

兩個方向(擇一):

A.callbackURL: '/auth/callback' + 透過 saveGenericReturnTo('/account/settings?open-delete=1') 指示 settings 頁自動重開 dialog 並跳到 confirm step。

B. 把 Google reauth 搬去獨立頁面 /account/settings/reauth(避免 dialog 依賴 mounted state)。

A 較簡單,沿用既有 saveGenericReturnTo bridge;B 架構更乾淨但範圍大。

Acceptance

  • 刪帳號走 Google reauth 路徑時不會中斷
  • 回到 /account/settings 後 dialog 自動重開且已通過 reauth 檢查
  • Passkey reauth 路徑無行為變化

TD-029 — mcp-toolkit alias fragility,shim 可能被 bypass

Status: done Resolved: 2026-04-25 — 方案 A 落地:新增 test/integration/mcp-production-wiring.spec.ts(8 tests),分兩組驗證:(1) 靜態檢查 nuxt.config.tsagents/mcp → shimmcpToolkitCloudflareProvider → mcpToolkitNodeProvider alias 規則 + shim 路徑 + toolkit cloudflare provider 仍用 agents/mcp specifier;(2) shim contract 行為驗證:POST initialize 回 Content-Type: application/json + valid JSON-RPC envelope(含 protocolVersion/capabilities/serverInfo),GET/DELETE 回 405 + Allow: POST。任一 alias drift 或 toolkit upstream 改 specifier 時 spec 立刻 fail。方案 B(patch upstream)留作長期計畫。 Priority: mid Discovered: 2026-04-24 — fix-mcp-streamable-http-session code-review MI-2 Location: nuxt.config.ts:312 alias mcpToolkitCloudflareProvider → mcpToolkitNodeProviderserver/utils/mcp-agents-compat.ts(shim) Related markers: 尚無 tasks.md marker(下一個該 change archive 前應登記到對應 tasks)

Problem

fix-mcp-streamable-http-session 的 fix(拒 GET/DELETE + 強制 JSON response)實作在 agents/mcp 的 shim(server/utils/mcp-agents-compat.ts)內。Shim 能生效依賴

  • @nuxtjs/mcp-toolkit 的 cloudflare provider(dist/runtime/server/mcp/providers/cloudflare.js)在執行時 await import("agents/mcp") 載入 shim
  • nuxt.config.ts 的 alias 將 agents/mcp 指到 shim 本身

風險:若 toolkit 將 provider 的載入方式改為其他 specifier(例如加 .js extension),或 nuxt.config.ts:312mcpToolkitCloudflareProvider → mcpToolkitNodeProvider alias 規則在某次 bundle 條件下命中,則 cloudflare provider 會被替換成 node provider,shim 不被載入:

  • node provider providers/node.js:61-63 自己回 405(但 response 格式不同)
  • node provider 預設 enableJsonResponse: false,POST 會走 SSE 路徑 → Workers 30s CPU hang 回歸
  • Claude re-initialize loop 重現

Fix approach

兩個方向(擇一或同時):

A. 加 production-wiring smoke test:在 built Nitro(preset cloudflare_module)下跑 POST /mcp initialize,斷言 Content-Type: application/json(非 text/event-stream)+ 完整 JSON-RPC response。確保 shim 真的被載入。

B. 把 fix 直接套到上游 @nuxtjs/mcp-toolkit cloudflare provider(PR 或 patch-package),取代 shim 這層。

A 輕量、快;B 根治但要維護 patch。建議先做 A 並登記 B 為長期計畫。

Acceptance

  • 方案 A:test/e2e/mcp-production-wiring.spec.ts 或等效,在 build 後驗證 Content-Type + response 完整性。若 shim 被 bypass 則 test 立刻失敗
  • 方案 B:上游 PR merged 或 patch-package 固定版本,shim 可 deprecated(但先保留避免 regression)

TD-030 — Claude.ai re-init 循環阻擋 tools/call(stateless 不足)

Status: done Priority: high Resolved: 2026-04-25 — wire-do-tool-dispatch v0.46.0 production flip + §6.4 4-layer fix(worker shim envelope inject / workerd HWM / nitro toNodeListener bypass / DO ReadableStream pattern)。staging acceptance 12/12 全綠,production worker fetch handler 正常驗證 bearer token 無 ownKeys/TypeError/5xx。 Discovered: 2026-04-24 — fix-mcp-streamable-http-session v0.37.0 post-deploy Claude.ai 實測 Location: MCP protocol layer(client=Claude.ai, server=server/mcp/index.ts + server/utils/mcp-agents-compat.tsRelated markers: @followup[TD-030]openspec/changes/fix-mcp-streamable-http-session/tasks.md 5.2–5.5 / 6.1 / 6.2 / 6.4

Problem

fix-mcp-streamable-http-session v0.37.0 上線後實測:

  1. GET /mcp405 Allow: POST,duration ~390ms(30s hang 消失)
  2. ✅ 首次 handshake 全綠:POST initialize 200 → notifications/initialized 202 → tools/list 200
  3. ✅ Claude.ai UI 顯示 "Loaded 4 Nuxt Edge Agentic RAG tools"
  4. ❌ 使用者按 AskKnowledge / SearchKnowledge / ListCategories → UI 顯示 "Error occurred during tool execution"
  5. ❌ wrangler tail 完全沒有 tools/call method 的 log

Tail 顯示的實際 pattern(使用者按任一 tool 之後):

POST /mcp initialize  400  (370ms)   ← Claude 自發 re-initialize
GET  /mcp             405  (390ms)
POST /mcp initialize  400
GET  /mcp             405
... 每 3 秒循環,tools/call 從未送達 ...

Claude 顯然把 GET 405 視為「stream 不可用 → 必須重建 session」,每次 tool call 前發新的 initialize。但第二次 initialize 的 body 被 MCP SDK transport 判為 invalid(可能是 Zod JSON-RPC schema parse fail 或 Server already initialized),回 400 → Claude 放棄 tool call。

這符合 design.md Fallback Plan 的情境:

"若 deploy 後 wrangler tail 觀察到 Claude.ai 對 405 仍有異常行為(例如不接受 405、視為網路錯誤 retry),按序處理..."

實務上 Claude 不是「不接受 405」,而是「每次 tool call 前都要求 session 可繼續」。純 Workers stateless 模式無法滿足此期待。

Fix approach

design.md / proposal.md 原案 Fallback Plan 的方向 A:

開新 change upgrade-mcp-to-durable-objects,以 Cloudflare Durable Objects 承載 session state + SSE stream 重寫 MCP layer。具體 transport(agents/mcp McpAgent + WorkerTransport,或自寫 DO-backed transport 直接組 WebStandardStreamableHTTPServerTransport + DO storage)待 /spectra-discuss + spike 收斂 — 已知 blocker:server/utils/mcp-agents-compat.ts 註解記載 agents/mcp WorkerTransport 在 production tools/call 遇 Cloudflare proxy ownKeys error,MUST 先 spike 驗證是否在 DO context 仍發生。Tier 3 重工,新增 DO binding、新 class、deploy pipeline 改動。

搭配動作:

  1. Wire up NUXT_KNOWLEDGE_FEATURE_MCP_SESSION feature flag(本 change 保留未用)讓 DO path 可漸進啟用
  2. Durable Object 承載 session state + server-initiated event 能力
  3. 保留 mcp-agents-compat.ts shim 的 GET 405 logic 作為 stateless fallback(例如 bearer-token-less probe)
  4. 向 Anthropic 回報 Claude.ai 對 stateless MCP server 的 re-init 行為是否符合 MCP spec 2025-11-25 意圖

Acceptance

  • upgrade-mcp-to-durable-objects change 上線後,Claude.ai 能穩定多輪 tool call(連續 3 次 AskKnowledge 不同 query 無 error banner)
  • wrangler tail 5 分鐘觀察:tools/call method 正常出現,無 POST initialize 400 循環
  • GET /mcp200 Content-Type: text/event-stream(DO 承載 SSE)或保留 405 — 具體策略隨 DO 設計決定
  • ChatGPT Remote MCP(若實測)同樣穩定

TD-040 — Token revoke 未同步清 MCP session Durable Object

Status: done Resolved: 2026-04-26 — add-mcp-token-revoke-do-cleanup change archived 於 openspec/changes/archive/2026-04-26-add-mcp-token-revoke-do-cleanup/。實作:(a) DO MCPSessionDurableObject.fetch() 開頭加 X-Mcp-Internal-Invalidate HMAC bypass → closeAllSseWriters + closeTransport + clearAllSseEvents + deleteAll + deleteAlarm;(b) DO initialize 寫 KV index mcp:session-by-token:<tokenId>;(c) admin revoke endpoint 加 cascadeInvalidateActiveSessions() best-effort cleanup(signing-key misconfig log.error 區分;KV/DO 失敗 swallow + log.warn)。HMAC trust anchor 沿用既有 NUXT_MCP_AUTH_SIGNING_KEY,無新 secret。已 ship v0.51.0 production deploy + smoke test 全綠。Evidence-based approval:integration spec 7/7 + unit 28/28 + 既有 DO lifecycle 22 tests + admin tokens route 9 tests 無 regression。 Priority: low Discovered: 2026-04-24 — upgrade-mcp-to-durable-objects Task 4.6 Location: server/api/admin/mcp-tokens/[id].delete.ts(revoke endpoint)+ server/durable-objects/mcp-session.ts(DO) Related markers: 本 change 完成後,tasks.md Task 4.6 保留 @followup[TD-040]

Problem

當管理員 revoke 一組 MCP token 時,該 token 已建立的 MCPSessionDurableObject session 不會被立即清除——僅依賴 DO 的 idle TTL alarm() 自然回收(預設 30 分鐘)。

影響:

  • Token revoke 後 token holder 若已有 live session,仍可在 TTL 剩餘時間內繼續以舊 session 呼叫 tools(雖然 middleware 會攔 token,但 DO session 認同 Mcp-Session-Id 而放行)
  • 稽核紀錄 revoke 時間與實際「停機」時間有差
  • Low priority:實務上 revoke 後 middleware 的 bearer-token 驗證會 401 擋下(token hash 對不上),所以 session 實際無法觸發 tool call

Fix approach

需要 token → sessionId 索引(目前不存在)。兩種方案:

  1. mcp_tokens 表加 active_session_ids TEXT[],issue token 時清空,每次 DO initialize 透過 admin API 把新 sessionId upsert 進來
  2. KV 以 mcp:session-by-token:<tokenId> 記錄 sessionId list,DO initialize 時寫入

Revoke endpoint 讀出 list → 迭代呼叫 env.MCP_SESSION.idFromName(sessionId).fetch(new Request('...', { method: 'DELETE' })) 讓 DO 清 storage。注意:目前 DO 把 DELETE 短路成 405,實作時要加一條 bypass(例如特定 X-MCP-Internal: invalidate header)或改用 POST /mcp/__invalidate

Acceptance

  • Admin UI revoke token 後 wrangler tail 觀察:對應 sessionId 的 DO 立即 deleteAll(或在 5 秒內)
  • 既有 DO TTL alarm 仍保留為 safety net
  • Integration test:token revoke → 同一 sessionId 的後續 request → 404

TD-041 — DO tool dispatch 未 wire up,flag=true 時 non-initialize 回假 ack

Status: done Priority: high Resolved: 2026-04-25 — wire-do-tool-dispatch v0.46.0 收尾。DO mcp-session.ts:handleGet/handleDelete/fetch 全鏈打通 + DoJsonRpcTransportenqueueAndPushServerNotification 串接 + 4 tool handler dispatch 經 DO 路徑實際 work(staging acceptance 4 tool call isError=false 全綠 + production deploy 後 worker 正常 verify bearer token)。 Discovered: 2026-04-24 — upgrade-mcp-to-durable-objects Phase 4 C-path scope trim(pre-rollout review 發現 tasks 4.3 [x] 與實作不一致) Location: server/durable-objects/mcp-session.ts:158-181(假 ack path);server/durable-objects/mcp-do-transport.tsDoJsonRpcTransport 已實作但未被 DO 呼叫) Related markers: @followup[TD-041]openspec/changes/upgrade-mcp-to-durable-objects/tasks.md Task 4.3 / 4.3.1 / 5.2;由新 change wire-do-tool-dispatch 全面接手

Problem

upgrade-mcp-to-durable-objects Phase 4(Pivot C)完成 DoJsonRpcTransport class + MCPSessionDurableObject session lifecycle(create / touch lastSeenAt / alarm GC / 404 on missing),但 DO fetch()non-initialize requesttools/list / tools/call / notifications/* 等所有 JSON-RPC methods)並未

  1. 呼叫 DoJsonRpcTransport.dispatch(envelope, extra) 把 message 丟給 SDK
  2. Lazy init McpServer + server.connect(transport) 讓 SDK 處理 tool 派遣
  3. 從 Nuxt 層序列化 auth context (event.context.mcpAuth) 並在 DO 內重建
  4. 從 DO env 取 Cloudflare bindings(D1 / KV / AI / BLOB)並注入 tool handler(目前 tool handler 透過 getCurrentMcpEvent() = Nitro useEvent() 取得 event.context.cloudflare.env,DO 內沒有 Nuxt H3Event)

Instead,non-initialize 目前回一個 { jsonrpc: '2.0', id, result: { session: { sessionId, lastSeenAt } } } 的假 ack。這意味著若 NUXT_KNOWLEDGE_FEATURE_MCP_SESSION=true 在 production flip 為 true,Claude.ai 對每個 tools/call 會看到 HTTP 200 但 response body 與實際 tool 無關 — silent degradation,比 tool 直接 fail 更難察覺

原 comment(server/durable-objects/mcp-session.ts:158-161)明確標示此為 follow-up:

Post-Phase 2 Pivot C: tool dispatch via DoJsonRpcTransport lands in a follow-up task (context plumbing to getCurrentMcpEvent). For now the session renewal path returns a minimal JSON-RPC acknowledgement so session lifecycle is observable end-to-end.

Fix approach

兩段處理:

短期(本 DO change 完成前必做,避免 rollout 隱患):把假 ack 改為明確的 JSON-RPC error(code -32601 Method not found-32603 Internal error),訊息指出「tool dispatch via DO 尚未實作,請設 NUXT_KNOWLEDGE_FEATURE_MCP_SESSION=false 走 stateless fallback」。production flag 永不 flip 為 true 直到長期方案落地;staging 用來驗 session lifecycle。

長期(新 change wire-do-tool-dispatch 接手)

  1. DO 內 lazy init McpServer 並 register 4 個 tool(複製 server/mcp/tools/*.ts 的 definition 或重構為可注入 context 的 pure function)
  2. Auth plumbing:Nuxt /mcp middleware 驗完 bearer token 後,將 McpAuthContext 序列化為特殊 header(例如 X-Mcp-Auth-Context,經 HMAC 簽章避免偽造)forward 到 DO;DO 內 parse + 重建 context
  3. Env plumbing:DO env 已有 D1 / KV / AI / BLOB binding(同 worker 共用),但 tool handler 的 getCurrentMcpEvent() 依賴 Nitro useEvent();需 fork tool handler 為 (args, context: { env, auth, ... }) => ... 形式,或在 DO 內建 shim event 滿足 event.context.cloudflare.env / event.context.mcpAuth 介面
  4. Reflect.ownKeys(env) workaround:shim 層的 installEnumerableSafeEnv 要同步在 DO runtime 套用(DO 的 env proxy 是否重現同 bug 需實測)
  5. Integration test:完整 e2e tools/call → DO → McpServer → tool handler → retrieval → JSON-RPC response 綠燈

Acceptance

短期 acceptance(本 DO change archive 前)

  • [ ] server/durable-objects/mcp-session.ts non-initialize path 回 JSON-RPC error(error.code = -32601-32603),body 包含「tool dispatch via DO not yet implemented」提示
  • [ ] test/integration/mcp-session-durable-object.spec.ts 新增 assertion:non-initialize 時 DO 回 error.code 非 null
  • [ ] Production NUXT_KNOWLEDGE_FEATURE_MCP_SESSION 維持 false;staging 僅用於 session lifecycle 驗證

長期 acceptance(wire-do-tool-dispatch change archive)

  • [ ] DO 內 tools/call 可完整執行 4 個 tool(askKnowledge / searchKnowledge / getDocumentChunk / listCategories)並回真實結果
  • [ ] Auth context 從 Nuxt 安全傳到 DO(帶簽章、不被偽造)
  • [ ] wrangler tail production 3 次 tool call 無 ownKeys error、無 re-init loop、回應帶正確 content
  • [ ] NUXT_KNOWLEDGE_FEATURE_MCP_SESSION=true 在 production flip 成功

TD-042 — Local NuxtHub dev KV binding 未注入 cloudflare.env/mcp 503

Status: done Resolved: 2026-04-25 — 新增 server/utils/local-kv-bridge.ts 提供 wrapHubKvAsNamespace(unstorage → KVNamespace shape,含 TTL emulation:{ value, expiresAt } envelope + lazy 過期清除)+ bridgeLocalKvOnEvent(event-context 注入器);新增 server/plugins/local-kv-bridge.ts thin Nitro plugin,guard process.env.NUXT_KNOWLEDGE_ENVIRONMENT === 'local',hook request 時注入 event.context.cloudflare.env.KV。production / staging Workers runtime 不受影響(plugin 直接 return)。新增 test/unit/local-kv-bridge.spec.ts 7 tests 覆蓋 round-trip / TTL 邊界 / non-expiring / production guard / 既有 binding 保留。 Priority: mid Discovered: 2026-04-24 — add-mcp-tool-selection-evals task 5.2 apply(首次跑 pnpm eval 時發現) Location: server/utils/cloudflare-bindings.ts:46 (getCloudflareEnv) / server/utils/mcp-middleware.ts:113 (getRequiredKvBinding) Related markers: search @followup[TD-042] in repo

Problem

本專案 MCP middleware 在 rate-limit 階段呼叫 getRequiredKvBinding(event, 'KV'),該 helper 從 event.context.cloudflare?.env[bindingName]globalThis.__env__[bindingName] 取 KV namespace。但 NuxtHub 的 local dev 用 fs-lite KV driver(.data/kv),並把此 driver 注入 cloudflare.env — NuxtHub 官方的 local bridge 只處理 D1(見 node_modules/@nuxthub/core/dist/module.mjs:523),不處理 KV。

結果:local pnpm devPOST /mcp 一定在 rate-limit lookup 時 throw 503 "Cloudflare KV binding \"KV\" is not available",任何經 middleware 的 MCP request 都走不完整個流程。影響:

  • pnpm eval(add-mcp-tool-selection-evals task 5.2)無法在 local 跑 baseline,暫時改指 staging
  • /admin/tokens mint 的 dev MCP token 在 local 無法驗證(token auth 過了,KV 層炸)
  • 任何 integration test 以外(非 in-process stub)的 local MCP 測試都撞到這個

production / staging 真 Cloudflare Workers runtime 不受影響(KV binding 由 Workers runtime 注入 cloudflare.env)。

Fix approach

加一個 nitro server plugin(local-only guard),在 request lifecycle 最前面把 NuxtHub 的 hubKV() wrap 成 KVNamespace-shaped object,注入 event.context.cloudflare.env[bindingName]

typescript
// server/plugins/local-kv-bridge.ts
export default defineNitroPlugin((nitroApp) => {
  if (process.env.NUXT_KNOWLEDGE_ENVIRONMENT !== 'local') return
  nitroApp.hooks.hook('request', (event) => {
    event.context.cloudflare ??= { env: {} }
    event.context.cloudflare.env.KV ??= wrapHubKvAsNamespace(hubKV())
  })
})

wrapHubKvAsNamespace 只要實作 get(key): Promise<string|null>put(key, value, { expirationTtl? }),match KvBindingLike interface(server/utils/cloudflare-bindings.ts:21)。也需要寫對應 unit test 覆蓋 expire TTL round-trip。

替代路線(較大改動但更根本):把 cloudflare-bindings.tsgetCloudflareEnv 改為對 KV 優先走 hubKV() → 不需 wrap 每個 request。但這讓 util 同時支援兩種 runtime,更難推理;偏好 plugin 方式讓 call site 不動。

Acceptance

  • Local pnpm dev 下,用有效 Bearer token curl -X POST http://localhost:3010/mcp 可走到 tools/list 並回 JSON-RPC success(不再 503)
  • Rate-limit 記錄實際寫到 .data/kv/,第 N+1 次請求被擋(fixed-window 驗證)
  • add-mcp-tool-selection-evals 可把 EVAL_MCP_URL 切回 http://localhost:3010/mcp 跑 baseline,且分數與 staging baseline 差異 ≤ 5pp(驗證行為一致)
  • 新增 test/integration/local-kv-bridge.spec.ts 覆蓋 wrapHubKvAsNamespace 的 get / put / ttl 行為

TD-043 — Evalite afterAll 的 process.exit / throw 不 propagate 到 pnpm eval

Status: done Resolved: 2026-04-25 — 採方案 2 wrapper script:新增 scripts/run-eval-with-exit.mjs(pure Node ESM,無第三方依賴;spawn pnpm exec evalite run test/evals/mcp-tool-selection.eval.ts + 即時 pipe stdout/stderr + buffer 累積;child exit 後若 combined buffer.includes('Eval regression:') 且 child 0 → exit 1,child non-zero → 保留 child code;SIGINT/SIGTERM forward 給 child)。package.jsoneval script 改 node scripts/run-eval-with-exit.mjseval:report / eval:watch 不變)。新增 test/unit/run-eval-with-exit.spec.ts 4 cases 覆蓋 decideExitCode 純函式(child 0 + 無/有 banner、child non-zero 保留)。test/evals/mcp-tool-selection.eval.ts 不動,wrapper 依賴既有 stderr banner。Acceptance(實際跑 eval 觸發 regression)留待使用者執行(會打 Anthropic API + AutoRAG,不可重現)。 Priority: low Discovered: 2026-04-24 — add-mcp-tool-selection-evals task 6.5 驗證(故意改壞 dataset 讓 overall < baseline − 5pp 時,stderr 有印 regression banner,但 pnpm eval 仍以 exit code 0 結束) Location: test/evals/mcp-tool-selection.eval.ts afterAll block,evalite@0.19.0 runtime Related markers: search @followup[TD-043] in repo

Problem

Design Decision 5 要求 overall score < baseline − 5pp 時 pnpm eval 以 non-zero exit code 結束(nightly / manual 回饋用)。實作嘗試了三條路線:

  1. process.exitCode = 1(afterAll)— evalite / vitest 不讀
  2. throw new Error('regression...')(afterAll)— 被 vitest / evalite 內部 wrapper catch
  3. console.error(...) + process.exit(1)(afterAll)— banner 在 stderr 出現、但 pnpm 仍看到 exit code 0(evalite 似乎 spawn child 並吃掉 exit code,或 vitest 的 afterAll 被 wrap)

三種方案 stderr signal 都有,但 exit code 都沒 propagate 到 pnpm / shell。目前 regression 只能靠 CI 腳本 grep log 的 Eval regression: 字串偵測,不是 Decision 5 描述的「exit non-zero」。

Fix approach

三條可能的 fix:

  1. 改用 evalite 原生 fail 機制:如果 evalite 有 scorer-level threshold(例如 { threshold: 0.5 } 讓 eval 標 failed),改把 regression 判斷放到個別 sample scorer 內,回傳 score: 0 + metadata.regression: true,讓 evalite 自身決定 exit。需查 evalite 0.19 支援情況
  2. Wrapper scriptpackage.jsoneval script 改成 node scripts/run-eval-with-exit.mjs,wrapper 跑 evalite 後 grep stdout 的 Eval regression: 字串,match 就 exit 1。簡單、與 evalite 版本解耦、但多一層 script
  3. 升 evalite 版本:檢查 0.20+ 是否修掉 afterAll throw swallow;若有直接升版

Acceptance

  • 故意改壞 dataset(overall < baseline − 5pp)後 pnpm eval; echo $?非 0
  • 正常 dataset(overall ≈ baseline)後 pnpm eval; echo $?0

TD-044 — session.create.before 靜默吞 user_profiles UNIQUE 衝突 → better-auth user id 與 user_profiles.id 可能漂移

Status: done Resolved: 2026-04-25 — fix-user-profile-id-drift change archived;session.create.before hook 改寫為 email_normalized-first lookup + app-level migrate children + non-production rethrow + actionable log hint,8 unit tests + live verify 全綠 Priority: mid Discovered: 2026-04-25 — consolidate-conversation-history-config §7.4 人工檢查(local dev 發訊息 → /api/chat 500 FOREIGN KEY constraint failed: SQLITE_CONSTRAINT_FOREIGNKEYLocation: server/auth.config.ts:487-513 (session.create.before hook 的 user_profiles sync try/catch) Related markers: search @followup[TD-044] in repo

Problem

better-auth 在 session.create.before hook 內 upsert user_profiles row(見 server/auth.config.ts:492):

typescript
try {
  await hubDb
    .insert(schema.userProfiles)
    .values({
      id: session.userId,
      emailNormalized,
      roleSnapshot: finalRole,
      adminSource,
    })
    .onConflictDoUpdate({...})
} catch (error) {
  authLog.error('user_profiles sync failed', {...})
  // 繼續,不 rethrow
}

user_profiles.email_normalizedNOT NULL UNIQUE(見 migration 0001_bootstrap_v1_core.sql),但 onConflictDoUpdate 的 target 是 PRIMARY KEY (id),不是 email_normalized。若資料庫中已有同 email_normalized 但不同 id 的 stale user_profiles row(通常因 better-auth user 表曾被手動刪 + 重建產生新 id),hook 會撞 UNIQUE(email_normalized) conflict → catch 吞掉 → 新 better-auth user 的 id 在 user_profiles 裡根本沒對應 row

後果:任何依 conversations.user_profile_id / query_logs.user_profile_id / messages.user_profile_id FK 的寫入都會 500:

D1_ERROR: FOREIGN KEY constraint failed: SQLITE_CONSTRAINT_FOREIGNKEY
at Object.createForUser (conversationStore)
at Object.chatHandler

Production Cloudflare D1 若沒發生過 user 刪除重建不會中鏢,但 local dev(多次 _dev/login / 手動 reset DB)很容易卡到 stale row,螢幕是詭異的「明明登入成功但 /api/chat 一直 500」。人工檢查 / screenshot review / 任何依 /api/chat 的 E2E 都會隨機失敗,且錯誤訊息「Chat failed」沒揭露 FK 真因。

Fix approach

三個選項(建議 1 + 3 雙保險):

  1. onConflictDoUpdate 的 conflict target:從 id → email_normalized,配合 SET id = excluded.id。這會讓 UNIQUE 衝突時改成「更新既有 row 的 id 指到新 user」,自然把 stale 接管。但要注意 children FK(conversations / messages / query_logs)需要 ON UPDATE CASCADE 才跟著動,否則變孤兒。
  2. 或先查後寫:插入前 SELECT id FROM user_profiles WHERE email_normalized = ?,有 stale → 決定是 update id(+ cascade children)還是 delete stale 再 insert;無則直接 insert。邏輯多但可控。
  3. 讓 catch 重新 throw(至少 in non-production)if (process.env.NODE_ENV !== 'production') throw。local 立刻 fail fast,生產保留現有保守行為。搭配 1/2 後這條幾乎不會炸,但若未來再有 drift 會立即暴露。

同時補:

  • log.error 輸出後加個 actionable hint(「可能是 user_profiles 有 stale row 指向舊 better-auth user;檢查 email_normalized = X 的既有 row」),省下次 debug 的時間。
  • migration 層面的 TD-009(email_normalized 改 nullable)不直接修本 bug,但配合 unique index 若加上 partial index (WHERE email_normalized IS NOT NULL) 可同時化解 passkey-only user 佔位衝突。

Acceptance

  • Local rm .data/db/sqlite.db && rm -rf .wrangler/state/v3/d1/... rebuild 後首次 /_dev/login + 發訊息 /api/chat 200
  • 手動 DELETE FROM user; INSERT INTO user (id, email, ...) VALUES ('new-id', same-email, ...)(或等同 better-auth 刪重建)後再登入 + 發訊息 /api/chat 200(不再 500)
  • 新增 unit 覆蓋:stub hubDb.insert 模擬 UNIQUE email_normalized 衝突,hook 走「改 id 路徑」把 user_profiles row 指到新 id,後續 conversations insert 不 FK fail
  • auth.config.ts 的 catch 在 non-production 改為 rethrow(或至少:error.log 有明確「stale user_profiles row 可能在 email=X」的 actionable hint)

TD-045 — Local dev bootstrap 連串斷點

Status: in-progress Priority: mid Discovered: 2026-04-25 — consolidate-conversation-history-config §7.4 人工檢查(新 session pnpm dev 後 /api/chat 一路從 500 FK error → 503 binding 未設 → 503 AutoRAG not found,共三關) Location: scripts/check-dev-bootstrap-health.mjs(predev 警告)、docs/tech-debt.md(本 entry)、待動:[nuxt-hub] DB binding not found 間歇 500 trace 定位 Related markers: search @followup[TD-045] in repo

Problem

Local dev 起床到 /api/chat 200 的路徑曾經有三條坑(原始發現時列入),經 v0.43.2 前後實測後收斂:只剩「.env NUXT_KNOWLEDGE_AI_SEARCH_INDEX 空值 → /api/chat 503」為穩定可重現,其餘兩條在目前 NuxtHub v0.10.7 路徑下不會出現。此外截圖審查過程浮現新症狀:/api/auth/me/credentials 間歇性 500 [nuxt-hub] DB binding not found,疑似 miniflare D1 binding cold-start / HMR race,未定位前歸入本 entry 的 active scope。

Status Update(2026-04-25, post-v0.43.2)

原始 Problem 三條坑經實測後收斂為 narrow scope:

  • Problem #1(NuxtHub 不 auto-apply migrations)obsoleteregressed/重新發現 @ 2026-04-25 fix-user-profile-id-drift apply 階段 cleanroom 實戰。先前 entry 描述「v0.10.7 已自動 apply」實際為 false:node_modules/@nuxthub/core/dist/db/runtime/plugins/migrations.dev.mjs 內 dev plugin 仍 gated by if (!hub.db.applyMigrationsDuringDev) return;,nuxt.config.ts hub: { db: 'sqlite' } 沒設此 flag,所以 cleanroom 後 _hub_migrations 僅建立空表、所有 migration 都未 apply。原本 entry 觀察到「11 筆應用紀錄」的環境是 v0.10.6 或更早跑過 migrate 後留存的狀態,cleanroom 重做時退回真空。Reproduction:rm .data/db/sqlite.db && rm -rf .wrangler/state/v3/d1/miniflare-D1DatabaseObject && pnpm dev,第一次 POST /api/_dev/login 500 Failed to prepare credential account(drizzle 對未存在的 user 表 SELECT 失敗)。手動 for f in server/database/migrations/*.sql; do sqlite3 .data/db/sqlite.db < $f; done 可灌進 schema 但 0007 ALTER 路徑跟 NuxtHub plugin 不對齊(產出 account.userId REFERENCES user_new(id) 殘留)。需 opt-in applyMigrationsDuringDev: true(簡單)或上游將其改 dev 預設值(需 NuxtHub PR)。
  • Problem #2(stale *_new FK refs):not-reproduced。當前 DB 的 sqlite_master 無任何 *_new(…) 殘留,PRAGMA foreign_key_check; 乾淨。此 bug 只會在手動 sqlite3 < migration.sql 路徑出現,正常 NuxtHub auto-apply 不會踩。v0.43.2 的 check-dev-bootstrap-health.mjs 已加 defensive 偵測(若 regresses,predev 會印 actionable warning + rebuild 指令)。
  • Problem #3(AI_SEARCH_INDEX= 空值):partial fix shipped @ v0.43.2 (00e5314)。predev 現會 warn NUXT_KNOWLEDGE_ENVIRONMENT=local + NUXT_KNOWLEDGE_AI_SEARCH_INDEX= 空值的組合,並指引 Notion Secret 頁「Local chat / AutoRAG 驗證指引」。script exit 0 不擋 dev。
  • 新增 active scope(從 HANDOFF Blocked 繼承)/api/auth/me/credentials 間歇性 500 [nuxt-hub] DB binding not found — 原 TD-045 未涵蓋,截圖審查時才浮現;需要可重現 trace 才能定位(疑似 miniflare cold-start race),本輪未碰。

Fix approach(remaining)

  1. NuxtHub dev migration auto-apply opt-innuxt.config.ts hub: { db: 'sqlite', applyMigrationsDuringDev: true }。最小成本,立刻讓 cleanroom 流程可跑。權衡:每次 dev startup 多跑 migration(idempotent,影響 < 1s),但 dev plugin 內部 try/catch 邏輯如果 migration 損壞會把錯誤帶到 startup(vs 目前是 lazy fail at request time)— 看做 fail-fast 優點。
  2. [nuxt-hub] DB binding not found 間歇 500 定位 — 收集一次重現 trace(timestamp / 其他同時請求 / miniflare 啟動 log)。可能方向:miniflare D1 binding hydration 的 race、HMR 後 server context 未重新綁定、@nuxthub/core v0.10.7 vs 舊版行為差異。
  3. .env.example 加註解 — 目前被 guard-check permanent-protected。若之後要動,使用者需手動改或解 guard;script output 已涵蓋指引,優先級低。
  4. cleanroom e2e 驗證 — Fix #1 後,rm -rf .data/db .wrangler/state/v3/d1/miniflare-D1DatabaseObject && pnpm dev 跑一次,確認全程無手動 sqlite3 步驟即可到 /api/chat 200。同時追跑 fix-user-profile-id-drift task 7.1 / 7.2 / 9.1 / 9.2(其 markers @followup[TD-045] 已登記)。

Acceptance

  • [x] sqlite3 .data/db/sqlite.db "SELECT count(*) FROM sqlite_master WHERE sql LIKE '%_new(%'" 回 0(驗證 2026-04-25)
  • [x] 若 .env 未設 NUXT_KNOWLEDGE_AI_SEARCH_INDEXpnpm dev 終端機清楚提示下一步(scripts/check-dev-bootstrap-health.mjs 實作,v0.43.2 00e5314
  • [ ] nuxt.config.ts hub.db.applyMigrationsDuringDev: true(或 NuxtHub upstream 把 dev 預設改 true)
  • [ ] rm -rf .data/db .wrangler/state/v3/d1/miniflare-D1DatabaseObject && pnpm dev 後首次 POST /_dev/login + /api/chat 直接 200(2026-04-25 實戰驗證為 500,根因 Problem #1 regression,等 opt-in 後重跑
  • [ ] fix-user-profile-id-drift task 7.1 / 7.2 / 9.1 / 9.2 在 cleanroom 修好後追跑通過
  • [ ] /api/auth/me/credentials 間歇性 500 已定位並修復(需 trace)

TD-046 — agentic-rag-staging AutoRAG index 在 CF 帳號中不存在

Status: done Resolved: 2026-04-25 — 透過 CF API 建 agentic-rag-staging AutoRAG(複製 production config,source agentic-rag-documents-staging,embedding @cf/qwen/qwen3-embedding-0.6b,sync_interval 21600s)+ 同名 AI Gateway。wire-do-tool-dispatch §7.1 4 個 tool call (askKnowledge x2 / searchKnowledge x2) 全部回 isError: false(empty results / refused 為 staging R2 空所致正常行為)。Staging R2 seed sample docs / production sync 屬於後續 RAG content 議題,不在本 TD scope 內,需要時另開 TD。 Priority: high Discovered: 2026-04-25 — consolidate-conversation-history-config §7.4 人工檢查(local /api/chat 503 AutoRAG not found;CF API 驗證) Location: wrangler.staging.jsonc.github/workflows/deploy.yml:238、Notion Secret 頁、agentic-rag-staging binding consumer Related markers: search @followup[TD-046] in repo

Problem

多個 config source 都宣稱 staging 環境用 agentic-rag-staging AutoRAG index:

  • wrangler.staging.jsonc NUXT_KNOWLEDGE_AI_SEARCH_INDEX=agentic-rag-staging
  • .github/workflows/deploy.yml:238 NUXT_KNOWLEDGE_AI_SEARCH_INDEX: agentic-rag-staging
  • Notion Secret 頁「環境總覽」staging row Search index: agentic-rag-staging
  • wire-do-tool-dispatch / upgrade-mcp-to-durable-objects 之 immediate validation 都假設 staging AutoRAG 能用

但 Cloudflare API GET /accounts/{account_id}/autorag/rags 實際只回一筆agentic-rag(production)。agentic-rag-staging 在 Dashboard / API 皆不存在。

可能原因:

  1. 規劃時寫入 config 但忘了在 CF 建
  2. 曾經有、後來刪但 config 沒清
  3. 建 staging AutoRAG 成本 / 資料 ingest 門檻讓團隊暫緩,但沒 flag 出來

影響:

  • Staging deployed worker 一旦被 end-to-end 測(真正 call /api/chat),會在 AutoRAG search step 503「AutoRAG not found」
  • MCP askKnowledge / searchKnowledge 在 staging 也會踩同樣錯
  • Local dev 若照 Notion 指引指向 staging,同樣被擋
  • wire-do-tool-dispatch 的 staging immediate validation(HANDOFF Next Steps 2-3)可能也 partial pass only — tools/list 過了但真 askKnowledge tool call 會死在 AutoRAG

Fix approach

兩個方向擇一:

  1. agentic-rag-staging AutoRAG index:到 CF Dashboard → AI → AutoRAG → 建 RAG,source agentic-rag-documents-staging(R2 bucket 已存在)、embedding / chunk size 與 production 對齊、AI Gateway 指 agentic-rag-staging(若 gateway 也沒建則同步建)。完成後執行 ingest(staging 資料集可以是 production 的子集)。
  2. 放棄 staging AutoRAG,把 staging 指向 production index:修 wrangler.staging.jsonc / deploy.yml / Notion 讓 NUXT_KNOWLEDGE_AI_SEARCH_INDEX=agentic-rag。風險:staging 讀到真實客戶資料,cache 命中 / 額度共用 production;需 risk review 是否 acceptable。

建議走 1(正確做法),時間壓力下走 2 並明確記錄。

Acceptance

  • CF API GET /accounts/{acc}/autorag/rags 回傳包含 agentic-rag-staging(若走方向 1)
  • 或 staging worker 部署後 curl -X POST .../api/chat 不再 503 AutoRAG not found
  • wire-do-tool-dispatch 的 immediate validation(HANDOFF §2.3)askKnowledge / searchKnowledge tool call 真的拿到 citation 不是 error

TD-047 — /api/chat SSE ready 後階段 error 時 Container 未 emit conversation-persisted

Status: done Resolved: 2026-04-25 — app/utils/chat-stream.ts 抽出 ReadChatStreamInput interface 並新增 onReady?: (data) => Promise<void> | void callback,handleEventBlockcase 'ready' 改為呼叫 await input.onReady?.(event.data) 後仍 return null(不阻 stream、不改 terminal flow)。app/components/chat/Container.vuependingConversation slot,submit 開始 reset、ready callback 寫入、catch block 在 kind !== 'abort' && pendingConversation 時 emit conversation-persisted fallback、finally 清除。abort 路徑刻意不 emit fallback(使用者主動取消不應 lock active id)。test/unit/chat-stream.test.ts +2 cases、test/unit/chat-container-streaming-contract.test.ts +3 cases 覆蓋 onReady invocation / ready-then-error path / fallback emit / submission 間 reset。 Priority: mid Discovered: 2026-04-25 — consolidate-conversation-history-config §7.4 debug Location: app/components/chat/Container.vue:228(try block emit 路徑)、app/utils/chat-stream.ts:146(ready event discard) Related markers: search @followup[TD-047] in repo

Problem

/api/chat 的 SSE 流程:

  1. Server 先把 conversation insert 到 DBconversationStore.createForUser
  2. 回傳 event: ready data: {conversationCreated: true, conversationId: ...}
  3. 接著跑 chatWithKnowledge(AutoRAG search、Workers AI 生成、judge…)
  4. 若 step 3 任何環節 throw → enqueue event: error

目前 Container.vue 的 client 實作:

  • readChatStreamready case return null(discard)
  • complete / refusal case return event → Container 從 terminalEvent.data.conversationId 讀 id 並 emit('conversation-persisted', ...)
  • error case throw new ChatStreamError → Container 走 catch block,不 emit

結果:DB 已經建了 conversation,但 UI 不知道;active conversation id 保持 null、sidebar historyRefreshKey 不 bump、使用者下一次發訊息又被當新對話 → DB 出現孤兒 conversation,user 只能 reload 才看得到。TD-046 的 staging AutoRAG 問題直接暴露這條 UX 缺口。

Fix approach

兩個層次一起修:

  1. chat-stream.tsready event 不要 discard,把 conversationCreated / conversationId 存進一個 handler ref。讓 readChatStream signature 多一個 onReady?: (data) => void callback 或在 return type 加 earlyReady: {...} 欄位。
  2. Container.vue
    • onReady callback 時先存 pendingConversation = {id, created}
    • 成功路徑繼續照舊 emit(拿 terminalEvent 的值)
    • catch block 裡若 pendingConversation 非空,用它 emit conversation-persisted(標記 message 為 error placeholder),讓 page / history 至少能 refresh active + sidebar
  3. 或者更簡單:server side 把 conversation create 從 handler 搬到 execute() 成功之後,避免孤兒 conversation。但這會失去 "streaming starts before DB write" 的 latency 優勢,需要 trade-off review。

Acceptance

  • 故意把 AutoRAG search 拔掉(例如 env AI_SEARCH_INDEX 指不存在的 index),發訊息後:
    • Sidebar 立刻出現新 conversation(或 Container 明確呼叫 DELETE /api/conversations/{id} 清 orphan)
    • 下一次發訊息會帶 conversationId 而不是建新 conv
  • Unit test:stub readChatStream 模擬 ready-then-error,驗證 Container emit conversation-persisted 且帶正確 id

TD-048 — 聊天 UI 缺顯式「新對話」入口

Status: done Resolved: 2026-04-25 — add-new-conversation-entry-points change archived;chat header 新對話按鈕 + reload 行為修復 + Safari private mode 相容(QuotaExceededError catch)+ e2e 5/5 + Design Review 完成 Priority: mid Discovered: 2026-04-25 — consolidate-conversation-history-config §7.2 人工檢查(使用者反映「找不到新對話按鈕」、「reload 始終停在同個對話」) Change: add-new-conversation-entry-points(archived 2026-04-25) Location: app/pages/index.vue(無明顯 CTA)、app/components/chat/ConversationHistory.vue:129+ icon 實際只 requestExpandapp/layouts/chat.vue header 也無新對話按鈕 Related markers: search @followup[TD-048] in repo

Problem

Chat UI 沒有任何顯式的「新對話 / New chat」按鈕或 menu item:

  • ConversationHistory.vue L129 的 i-lucide-plus 按鈕 aria-label "新增對話" 只在收合的 sidebar 出現,@click="requestExpand" 只做展開 sidebar 的事,不真的重置 active(容易誤會)
  • 展開狀態 sidebar 沒有 + 按鈕
  • 頂部 nav 的「問答」link to="/" 在已有 active 的狀況下 navigate 到 / 不會清掉 sessionStorage 裡的 active id,restoreActiveConversation 會把它讀回來
  • 使用者唯一的「開新對話」路徑是:刪除當前 active conversation,或手動清 sessionStorage['web-chat:active-conversation:<userId>'] 後 reload

人工檢查時使用者反映「我找不到新對話按鈕」、「整頁重整始終在同個對話」— 屬於正當 UX 需求被封死。這不只阻擋 review flow,也會讓真實使用者卡住(想新開一個主題的對話就做不到)。

Fix approach

三個層次擇一 / 並用:

  1. 明顯的 CTA:在展開 sidebar 的 header 右側加 UButton icon="i-lucide-plus" aria-label="新對話",click → 呼叫 handleConversationCleared()(index.vue 已有這 handler,做的事就是 setActiveConversation({conversationId: null, messages: []}))。成本最低、語意最清楚。
  2. Header-level 入口:chat layout header 加一個「新對話」按鈕,mobile / desktop 都能看見;click 路徑同上。
  3. 「問答」nav link 行為修正:若 active 非 null 且 click /,視為清空 active 的 intent(也要清 sessionStorage)。比較隱晦,不建議單用,但可作為輔助。

額外 UX 考量:clear 的 confirm flow — 若使用者誤點要不要有 undo / 雙層確認?目前 active 被切換時就 drop 使用者剛打字未送出的 input;需補一層 dialog 或改 setActiveConversation 不清 draft。

Acceptance

  • 展開 sidebar 可見「新對話」按鈕(desktop + mobile drawer 都有)
  • 按下後主面板回到 hero empty state、sidebar active 高亮消失、sessionStorage 內 web-chat:active-conversation:<userId> 被刪
  • reload 後不會把剛開新對話的狀態覆寫回舊的
  • 新增 E2E / unit test 覆蓋「建 conv A → 按新對話 → 建 conv B,兩筆各自獨立」

TD-049 — Cloudflare Pages deploy API 拒絕 git HEAD commit message

Status: done Resolved: 2026-04-26 — workaround commit 5ce334c(顯式傳 --commit-message "Deploy <SHA>"--commit-hash "<SHA>",其中 <SHA> 為 GitHub Actions github.sha context 的 expression 寫法,詳見 .github/workflows/deploy.yml)落地後,至 v0.50.1 ship 為止 GitHub Actions Deploy workflow 連續 30 條 run 全綠(過去最近抽查 gh run list --workflow=Deploy -L 30 -- --jq 'select(.conclusion=="failure")' = 0 hits),workaround 已驗證對普遍 commit message 有效。原 acceptance 第 2 條(v0.43.0 manual rerun)省略 — 後續 30+ 次 deploy 已遠超 acceptance 第 3 條的「3 次穩定」門檻。 **Priority**: mid **Discovered**: 2026-04-25 — v0.43.0 release deploy run [24908303837](https://github.com/YuDefine/nuxt-edge-agentic-rag/actions/runs/24908303837) **Location**: .github/workflows/deploy.ymldeploy-docs-production/deploy-docs-stagingDeploy docs to Cloudflare Pagesstep **Related markers**: search@followup[TD-049]` in repo

Problem

wrangler pages deploy 在不顯式傳 --commit-message 時會把 git HEAD commit subject + body 附到 Cloudflare Pages API 的 deployment record。對某些 commit message,Cloudflare API 會回:

✘ [ERROR] A request to the Cloudflare API (/accounts/.../pages/projects/.../deployments) failed.
  Invalid commit message, it must be a valid UTF-8 string. [code: 8000111]

導致 deploy-docs-production / deploy-docs-staging 失敗,docs 站(agentic-docs.yudefine.com.tw)停留在前一版。app worker deploy 不受影響(走 wrangler deploy --config server/wrangler.staging.json,不經 Pages API)。

重現條件(2026-04-25 觀察)

CommitTypeDocs Pages deploy特徵
5a47a63 v0.43.0 tagtag push❌ 8000111emoji + 繁中 + 全形括號 + 頓號 + TD-044~048
a0e2426 更新 HANDOFFmain push也是 emoji + 繁中 + 全形標點 + 美式引號

兩個 commit message 經 git log --format='%s%n%b' <sha> \| xxd 檢驗皆為合法 UTF-8,所以 Cloudflare Invalid UTF-8 string 的錯誤訊息本身不精確——非 wrangler 4.84.1 全面漂移,也不是本機 encoding 問題。觸發條件疑為 CF API 某個未文件化的 validator,可能針對:

  • 特定字元組合(例如 TD-044~048 的 ASCII tilde + 全形括號)
  • body 的特定格式(換行 + 前導空白 + list marker 的組合)
  • body 長度或某個 byte sequence

Fix approach

Short-term (this TD).github/workflows/deploy.yml 的兩個 deploy-docs-* step 顯式傳:

yaml
# 下列 <SHA> 為 GitHub Actions context expression `github.sha`(實際 yaml 寫法
# 為 dollar 符 + 雙大括號包 github.sha;此處改用占位符,避免 docs site 的
# Vue SSR compiler 把字面 mustache 誤判為模板表達式)。
run: pnpm exec wrangler pages deploy docs/.vitepress/dist \
  --project-name "$DOCS_CF_PAGES_PROJECT_NAME" \
  --branch "$DOCS_CF_PAGES_*_BRANCH" \
  --commit-hash "<SHA>" \
  --commit-message "Deploy <SHA>"

實際 GitHub Actions expression 以 .github/workflows/deploy.yml deploy-docs-* job 為準。避開 git HEAD commit message 直接作為 CF API 輸入。--commit-hash 仍保留 SHA 供 CF dashboard 顯示。

Long-term — 若此問題在其他專案重現、或未來要恢復用 git commit message(保留 dashboard 可讀性),整理:

  • 5a47a63 完整 body bytes(已在 docs/solutions/tooling/2026-04-25-cloudflare-pages-utf8-commit-message.md
  • 一個最小 repro(4 個字元的 commit message 還會觸發嗎?)
  • 開 issue 於 cloudflare/workers-sdk 或 CF Pages support

Acceptance

  • [x] .github/workflows/deploy.yml 兩個 deploy-docs-* job 的 wrangler pages deploy--commit-message + --commit-hash(commit 5ce334c
  • [x] 重跑 v0.43.0 docs production deploy — 省略,後續 30+ deploy run 已驗 workaround 普適
  • [x] 後續 3 次 main push / tag 發版,deploy-docs-* 皆綠(實際達 30+ 次連綠)
  • [ ] (Optional)保存 5a47a63 完整 bytes + 最小 repro,供未來向 workers-sdk / CF 回報

TD-050 — Production / staging demo seed evidence bridge

Status: done Resolved: 2026-05-04 — 新增 scripts/demo-seed/ + pnpm demo-seed <staging|production>,並已套用到 staging 與 production。兩個環境都有 12 份 demo documents、14 個 versions、94 個 source chunks、12 筆 citation_records、16 筆 query_logs、18 筆 messages、5 個 demo users、4 個 MCP token rows;R2 寫入 108 個 demo objects;AI Search sync job 完成(staging ae14e71d-d066-45e5-87b2-e44c41a9f3d4、production 20d8bc95-e45f-44a0-99e7-1318c1fea922)。pnpm demo-seed <env> --verify-only 透過 Worker AI binding structured filters 驗證 internal / restricted 查詢各回 5 筆 active/current 且含 document_version_id / citation_locator / access_level metadata 的 chunks。 Priority: mid Discovered: 2026-04-25 — wire-do-tool-dispatch §7.1 post-fix observation(TD-046 修復後 4 個 tool call isError: falsecitations:[] / results:[] empty) Location: scripts/demo-seed/, docs/runbooks/demo-seed.md, R2 buckets agentic-rag-documents-staging / agentic-rag-documents, AI Search instances agentic-rag-staging / agentic-rag, D1 agentic-rag-db-staging / agentic-rag-dbRelated markers: search @followup[TD-050] in repo

Problem

TD-046 修復後 staging agentic-rag-staging binding 已可正常呼叫。2026-04-25 已上傳 5 份 fixture 到 R2;2026-05-04 live audit 進一步確認 wrangler ai-search stats agentic-rag-staging 顯示 Indexed=5、R2 bucket object_count=5 / bucket_size=18 kB

但 staging app 仍無法當成完整 RAG demo 環境,因為 D1 agentic-rag-db-staging 當時沒有對應文件中繼資料:

  • documents=0
  • document_versions=0
  • source_chunks=0
  • query_logs 40 筆中 33 筆是 decision_path='no_citation_refuse'

目前直接打 wrangler ai-search search agentic-rag-staging 可以查到 staging-seed/procurement-manual.md,但結果沒有 app 所需的 document_version_id / citation_locator metadata;而 app retrieval pipeline 會在 AI Search 後做 D1 post-verification。缺 D1 evidence bridge 時,所有 staging RAG retrieval 仍會回 empty / refused:

  • askKnowledge{"citations":[],"refused":true}(無 evidence ⇒ refused,符合 retrieve-then-answer 設計)
  • searchKnowledge{"results":[]}(無 D1 verified evidence ⇒ empty hits)

對於 wire-do-tool-dispatch §7.1 immediate validation 而言,這仍滿足「非 501 / 非 re-init / isError: false」門檻,不阻擋 archive;但若使用者:

  1. 走 staging 做端到端 chat 體驗測試
  2. 從 local dev 指向 staging 跑 pnpm eval
  3. 想用 staging 重現 production RAG 行為

都會發現實質上「拿不到答案」。

2026-05-04 已改成 production / staging 共用的 demo seed contract,並補齊 app 所需的 D1 evidence bridge 與 R2 chunk metadata。Production 也同步擴充,避免正式展示環境只有採購流程與少量 smoke docs。

Fix approach

已落地:

  • scripts/demo-seed/demo-seed-data.ts 定義 deterministic seed contract 與 feature coverage。
  • scripts/demo-seed/demo-seed-worker.ts 用 remote Wrangler worker 寫入正式 D1 / R2 binding,R2 chunk object 帶 statusversion_stateaccess_leveldocument_version_idcitation_locator
  • scripts/demo-seed/run-demo-seed.ts 封裝 dry-run、apply、AI Search sync、AI binding structured-filter verification。
  • docs/runbooks/demo-seed.md 記錄重跑與驗證方式。

Acceptance

  • [x] Staging / production D1 具備 demo 文件:各 documents=12document_versions=14source_chunks=94、current versions 11
  • [x] Staging / production R2 具備 demo source + normalized chunks:各 108 個 demo objects;末端 chunk normalized-text/demo-<env>-ver-legacy-procurement-2024-v1/0006.txt 可下載。
  • [x] AI Search sync 完成:staging job ae14e71d-d066-45e5-87b2-e44c41a9f3d4、production job 20d8bc95-e45f-44a0-99e7-1318c1fea922
  • [x] Worker AI binding structured filters 驗證:internal 查詢「PR 和 PO 的差別」與 restricted 查詢「restricted access 可以查哪些預算資料」在兩個環境各回 5 筆 active/current chunks,且 metadata 完整。
  • [x] Seed 來源與維護方式記錄於 docs/runbooks/demo-seed.md

Progress

2026-04-25:已用 wrangler r2 object put --remote 上傳 5 份 static fixture 至 agentic-rag-documents-staging/staging-seed/

  • README.md
  • procurement-manual.md
  • hr-policy.md
  • system-faq.txt
  • procurement-manual-v2.md

2026-05-04 live audit:agentic-rag-staging AI Search 已有 Indexed=5,但 D1 agentic-rag-db-staging 仍是 documents=0 / source_chunks=0;staging search 對「採購流程操作手冊」可命中 staging-seed/procurement-manual.md,但對「員工請假辦法」、「差旅費用報銷」、「新人入職指南」也只回同一份 procurement 檔,且無 metadata。尚未關閉本 TD:下一步不是單純等 indexing,而是把 staging fixture 補進 app 的 D1 evidence / current-version 鏈,再重跑 tool call 驗證。

2026-05-04 resolution:已補 production / staging 共用 demo seed,並在兩個環境驗證 D1 / R2 / AI Search / HTTP:

  • D1:兩環境各 documents=12active_documents=11archived_documents=1versions=14current_versions=11chunks=94citations=12query_logs=16messages=18conversations=5tokens=4users=5role_changes=4
  • R2:兩環境末端 normalized chunk 可下載,內容為封存文件稽核用途片段。
  • AI Search binding:兩環境 internal / restricted probe 都只回 active/current chunks,且包含 app 所需 citation metadata。
  • HTTP:https://agentic.yudefine.com.twhttps://agentic-staging.yudefine.com.tw 皆回 200。

TD-051 — libsql legacy_alter_table=1 破壞 0007/0009 的 RENAME-rewrite 假設

Status: done Priority: high Discovered: 2026-04-25 — dev server 報 unable_to_link_account(better-auth Google OAuth 登入路徑) Location:

  • 根因:server/database/migrations/0007_better_auth_timestamp_affinity.sql:464-471server/database/migrations/0009_passkey_and_display_name.sql:116-218 的 RENAME-rewrite 假設
  • 修正:server/database/migrations/0012_fk_rebuild_user_references.sql
  • 影響表:account / session / passkeyuserId FK) Related markers: 無 tasks.md marker(直接修進 migration 0012;本 entry 為 done 狀態的歷史紀錄)

Problem

Migration 0007 / 0009 用 SQLite 的 table-rebuild 慣用法:建 *_new 子表寫成 userId TEXT NOT NULL REFERENCES user_new(id),再對 user_newALTER TABLE user_new RENAME TO "user",預期 SQLite 會在 RENAME 時把所有子表的 FK 文字從 user_new 自動改寫成 "user"

這個自動改寫只在 modern SQLite 行為PRAGMA legacy_alter_table = OFF)才會發生。0007 留了註記說「Verified 2026-04-20 on local D1」、「modern behaviour,default on D1」。

但 NuxtHub local dev 用的是 libsql backend,libsql 預設 legacy_alter_table = 1(legacy 行為,RENAME 不改寫子表 FK 文字)。後果:

sqlite> PRAGMA legacy_alter_table;
1
sqlite> SELECT sql FROM sqlite_master WHERE name IN ('account','session','passkey');
CREATE TABLE "account" (
  ...
  userId TEXT NOT NULL REFERENCES user_new(id) ON DELETE CASCADE,
  ...
)
-- session、passkey 同樣殘留 user_new ref

任何對 account / session / passkey 的 INSERT 都炸:

$ sqlite3 .data/db/sqlite.db "INSERT INTO account ... VALUES (...)"
Error: in prepare, no such table: main.user_new

better-auth 在 OAuth callback 拼裝 account row 時拋出例外,路徑:@better-auth/oauth2/link-account.mjsc.context.internalAdapter.linkAccount(...) → catch → return { error: "unable to link account" }/api/auth/error?error=unable_to_link_account

對使用者影響:fresh local DB 跑 Google OAuth 登入直接被擋,無法建立 session。

Fix approach

新增 0012_fk_rebuild_user_references.sql,對三張表做 explicit-FK rebuild:

  • CREATE TABLE *_v12 (..., userId ... REFERENCES "user"(id) ...) — 直接寫 "user",不依賴 RENAME-rewrite
  • INSERT INTO *_v12 SELECT ... 從舊表搬資料
  • DROP TABLE old; ALTER TABLE *_v12 RENAME TO old
  • recreate 索引(account_userId_idxpasskey_credentialID_idxpasskey_userId_idx
  • 開頭設 PRAGMA legacy_alter_table = OFF 防禦未來新表 RENAME

對 production D1(FK 文字已是 "user")→ 此 migration 是慢 no-op;對 local libsql(FK 殘留 user_new)→ 修復成正確 ref。結果上 idempotent

Acceptance

  • [x] sqlite_master 中無任何含 user_new 的 schema text(SELECT name FROM sqlite_master WHERE sql LIKE '%user_new%'; 為空)
  • [x] INSERT INTO account (id, userId, ...) VALUES ('test', '<existing-user-id>', ...) 不再報 no such table: main.user_new
  • [x] PRAGMA foreign_key_check; 乾淨
  • [x] account_userId_idxpasskey_credentialID_idxpasskey_userId_idx 三個索引已 recreate
  • [ ] 重啟 pnpm dev 後走 Google OAuth 登入,better-auth 不再回 unable_to_link_account(待使用者驗證)
  • [ ] Production D1 fk_check 仍乾淨、無資料漂移(待 deploy 後驗證;預期為 no-op)

Status: done Resolved: 2026-04-25 — fix-user-profile-id-drift claim 接手後修正:(1) schemaFake 補 4 張 child table descriptor (conversations / queryLogs / messages / documents);(2) select.from(table) 改為 table-aware,mocks.hubDbSelect(table)user / user_profiles 路由不同 state;(3) hubDbSelectuser_profilesstate.profiles 避免誤入 migrate path;(4) mocks.hubDbUpdateuser_profiles 表 update 處理寫回 state.profiles。整套 pnpm test:integration 全綠(81 files / 435 tests / 1 skipped)。 Priority: low Discovered: 2026-04-25 — wire-do-tool-dispatch §5.x SSE Tests 跑 pnpm test:integration 時 cross-spec failure(fix-user-profile-id-drift change 已 wire syncUserProfile 進 hook,但既有 passkey-first-link-google.spec.tshubDb.transaction stub 不覆蓋 migrate path 的 tx.update(schema.conversations) 呼叫) Location: test/integration/passkey-first-link-google.spec.ts:116Object.transaction stub)+ server/utils/user-profile-sync.ts:73(migrate transaction 第一個 update) Related markers: search @followup[TD-052] in repo

Problem

fix-user-profile-id-drift change 把 session.create.before hook 改成呼叫 syncUserProfile,內含 db.transaction(async (tx) => { tx.update(schema.conversations).set(...).where(...) ... }) migrate path(當 stale user_profiles.email_normalized 撞到不同 id 時觸發)。

passkey-first-link-google.spec.tshubDb mock 在 transaction callback 內沒提供 tx.update(...) 完整鏈式 stub — 第一個 .update(schema.conversations) 就拋 TypeError: Cannot read properties of undefined (reading 'userProfileId')

實質效果:allowlist reconciliation:綁定 allowlist email 後,下次 session refresh 升 admin 並寫 audit 此 test 在跑 runSessionRefresh 時誤觸 migrate path(mock fixture 的 setup 與 stale-row 情境意外重疊,非測試設計)。

不阻擋以下事項:

  • wire-do-tool-dispatch 自身 archive(TD-052 與該 change 無關)
  • fix-user-profile-id-drift 自身的 8 個 unit test 在 auth-user-profiles-sync.spec.ts 仍全綠

阻擋:

  • pnpm check 全套 → integration test 1 failed
  • 邏輯上應由 fix-user-profile-id-drift archive 前收尾(該 change 引入 syncUserProfile,但人工檢查或 spec 沒覆蓋既有 spec 的 mock collateral)

Fix approach

兩種選擇:

  1. Mock 層:擴 passkey-first-link-google.spec.tshubDb.transaction stub — 在 transaction callback 內提供完整 tx.update().set().where() chain,回傳 mock query builder(最快、scope 最小)
  2. Setup 層:讓 passkey-first-link-google.spec.ts 的 user fixture 在 email_normalized 上不撞 stale row — 確保 syncUserProfile 走 (id 相同 → UPDATE non-id 欄位) 路徑、不進 migrate transaction(更貼近實際運行情境,但需要對 setupAuthDatabase fixture 對齊)

建議走 1:mock 層改動侷限在單一 spec、不影響其他 fix-user-profile-id-drift 的 unit tests。

Acceptance

  • [x] pnpm test:integration test/integration/passkey-first-link-google.spec.ts 全綠
  • [x] 既有 auth-user-profiles-sync.spec.ts 8 個 spec 仍綠(不退步)
  • [x] 由 fix-user-profile-id-drift change 收尾(task §8.4 + @followup[TD-052] marker;2026-04-25 done)

TD-053 — fix-user-profile-id-drift production observation(archive 後 follow-up)

Status: done Resolved: 2026-04-26 — v0.50.1 production 立即驗收(取代原 1 週觀察期):過去 7 天 better-auth session.createdAt = 0 條(無新 sign-in 流量)→ session.create.before hook 未被觸發 → 無 user_profiles sync failed 訊號可採。Live wrangler tail 5 分鐘採樣(user 已登入發 6 條 chat,期間 /api/auth/get-session × 2 + /api/conversations × 多次 + POST /api/chat × 6)也未出現任何 user_profiles sync failed log。Hook catch path 在無流量期間穩定,fix-user-profile-id-drift 部署後無 regression 訊號。附帶發現:production 有 6 條 orphaned user_profilesprofile_count=23 vs user_count=17,profile.id 不在 user.id),屬 v0.49.0 / fix-user-profile-id-drift 前的歷史殘留(schema 中 user_profiles.id 並非 user.id 的 FK,無 cascade 清理機制);不阻擋 TD-053 標 done,獨立追蹤於 TD-058。 Priority: low Discovered: 2026-04-25 — fix-user-profile-id-drift archive 時 §9.3 條目轉為 archive 後 follow-up Location: production wrangler tail / Cloudflare logs Related markers: search @followup[TD-053] in repo

Problem

fix-user-profile-id-drift 在 local dev 已驗證 hook INSERT + migrate 兩個 branch 行為正確(archive tasks §7.1 / §7.2 + 8 unit tests)。production 部署後須立即採樣 wrangler tail 確認 hook catch 路徑沒被預期外場景觸發(觸發代表有 stale row 情境在 production 真實出現,須讀 hint 進一步分析)。

Fix approach

部署 fix-user-profile-id-drift 至 production 後立即採樣(取代原「1 週觀察期」):

  1. wrangler tail --env production --format json | jq 'select(.message[]? | contains("user_profiles sync failed"))'(或在 evlog drain 過濾此 wide event 字面值)採樣 5-10 分鐘 + 撈 evlog drain 最近 24h
  2. 計數出現次數
  3. 若 > 0:讀每筆的 hint 欄位(已包含 redacted email + 固定 stale-row 提示),判斷是否為預期 corner case(手動刪 user 重建)或新 regression
  4. 若 corner case:紀錄 → close。若 regression:開新 change 補強 syncUserProfile 邏輯

Acceptance

  • [x] Deploy fix-user-profile-id-drift 至 production(含於 v0.49.0 release commit 9805e2f
  • [x] 立即採樣 — 2026-04-26 v0.50.1 deploy 後撈 production session 表:過去 7 天 session.createdAt = 0 條,hook 未被觸發;無流量等於無 regression 訊號。Live wrangler tail 採樣為佐證(user 自跑),結果若有觸發再加註此 entry
  • [x] Status 標 done(無 regression);附帶發現 6 條 orphaned profile 開 TD-058 獨立追蹤

TD-054 — add-new-conversation-entry-points Safari private mode 實機驗證

Status: open Priority: low Discovered: 2026-04-25 — add-new-conversation-entry-points archive 時 §7.6 使用者授權 skip Location: app/utils/chat-conversation-state.ts clearConversationSessionStorage (try/catch QuotaExceededError) Related markers: search @followup[TD-054] in repo

Problem

add-new-conversation-entry-points archive 時,§7.6「Safari private mode 點任一新對話按鈕 → 仍能進新對話畫面、無 toast、無 console error」未本機 Safari 實測。clearConversationSessionStorage helper 已內建 try/catch 涵蓋 QuotaExceededError / DOM Storage disabled 等 edge case,理論上安全,但缺實機評估。

Fix approach

打開 Safari → 開啟「私密瀏覽」視窗 → 進入 production / staging 對話頁面 → 走過三處新對話入口(chat header、sidebar expanded header、sidebar collapsed plus)。確認:

  • 點按鈕後成功進入新對話畫面(messages 清空、active state reset)
  • 無 toast notification 跳出
  • DevTools console 無 error log(QuotaExceededError 應被 helper try/catch 吞掉)

Acceptance

  • [ ] Safari private window 三入口各跑一次成功
  • [ ] DevTools console 無 error
  • [ ] Status 標 done

TD-055 — TD-051 漏網之魚:3 張表的 FK 殘留 _new ref(mcp_tokens_new / query_logs_new)

Status: done Resolved: 2026-04-26 — fix-fk-rebuild-query-logs-chain change 落地:migration 0015(server/database/migrations/0015_fk_rebuild_query_logs_chain.sql)仿 0012 explicit-FK rebuild pattern 重建 query_logs / messages / citation_records 三張表,FK 文字改為 canonical mcp_tokens(id) / query_logs(id)。Spec 補一條 ADDED Requirement「Live DDL Foreign Key References Match Canonical Table Names」進 auth-storage-consistency,把「stored DDL 不得殘留 *_new 字樣」變可測試契約(SELECT name FROM sqlite_master WHERE sql LIKE '%REFERENCES %_new(%' 必須回 0 列)。Synth-broken 端對端驗證:bug 重現(FK on INSERT 炸 no such table: main.mcp_tokens_new)→ 套 0015 → 修復 → INSERT 成功 + foreign_key_check 乾淨 + row counts 保留 + 五個 named index 全在。Production verify (2026-04-26 v0.50.1 deploy 後)wrangler d1 execute agentic-rag-db --remote 跑 stored DDL 檢查回 0 列 + PRAGMA foreign_key_check 回 0 列,與預期 no-op 一致(archive task 2.8 / 4.5 補驗)。 Priority: high Discovered: 2026-04-26 — add-sse-resilience §7.1 local heartbeat 驗證時連環炸:

  1. POST /api/chatcreateQueryLog 階段 throw SQLITE_ERROR: no such table: main.mcp_tokens_new
  2. 修第一個 FK 後重試,又炸 createMessageSQLITE_ERROR: no such table: main.query_logs_newLocation:
  • 根因:server/database/migrations/0010_fk_cascade_repair.sql:157 + 同 migration 內 messages / citation_records rebuild 階段都寫 REFERENCES *_new(id),假設後續 RENAME 會自動改寫
  • TD-051 修了 account / session / passkey(migration 0012),但漏掉 query_logsmessagescitation_records 三張
  • 三張漏網表
    • query_logs.mcp_token_idmcp_tokens_new(應為 mcp_tokens
    • messages.query_log_idquery_logs_new(應為 query_logs
    • citation_records.query_log_idquery_logs_new(應為 query_logs

Related markers: 無 tasks.md marker(本 entry 為發現紀錄;fix 會在獨立 change 處理)

Problem

與 TD-051 完全相同的根因:libsql 預設 legacy_alter_table = 1ALTER TABLE x_new RENAME TO x 不會自動把其他子表的 FK 文字 x_new 改寫成 x。Migration 0010 的多張 rebuild 仍寫 REFERENCES *_new(id),RENAME 後 FK 文字殘留。

sqlite> .dump | grep "REFERENCES [a-z_]*_new("
  mcp_token_id TEXT REFERENCES mcp_tokens_new(id) ON DELETE SET NULL,        -- query_logs
  query_log_id TEXT REFERENCES query_logs_new(id) ON DELETE SET NULL,         -- messages
  query_log_id TEXT NOT NULL REFERENCES query_logs_new(id) ON DELETE CASCADE, -- citation_records

sqlite> SELECT name FROM sqlite_master WHERE name IN ('mcp_tokens_new', 'query_logs_new');
(no rows)

任何 INSERT 進這三張表都炸 SQLITE_ERROR: no such table,連環擋掉 web chat / MCP 工具呼叫的 query_log + message + citation 寫入路徑。

對 production D1 不影響(D1 的 SQLite 行為是 modern,FK 已正確改寫);對 local libsql fresh DB 全炸。

Fix approach

仿 migration 0012 的 explicit-FK rebuild pattern,新增 0015_fk_rebuild_query_logs_chain.sql(0013 / 0014 已被 messages refused/refusal_reason 擴欄佔用):

  • 開頭 PRAGMA legacy_alter_table = OFF + PRAGMA defer_foreign_keys = ON
  • 對三張表分別 rebuild:
    • query_logs_v15(FK → mcp_tokens(id)
    • messages_v15(FK → query_logs(id)
    • citation_records_v15(FK → query_logs(id)
  • 每張:CREATE → INSERT SELECT → DROP old → RENAME → recreate indexes
  • recreate 索引:
    • idx_query_logs_channel_created_at
    • idx_messages_query_log_ididx_messages_conversation_created_at
    • idx_citation_records_query_log_ididx_citation_records_expires_at
  • 收尾 PRAGMA foreign_key_check

對 production D1(FK 文字已是正確名稱)→ 慢 no-op;對 local libsql(FK 殘留 _new)→ 修復成正確 ref。結果上 idempotent

順帶複檢全 schema:sqlite3 .data/db/sqlite.db ".dump" | grep -E "REFERENCES [a-zA-Z_]+_new\(" 應為空。本 entry discovery 時用此檢查確認只剩這三張。

Acceptance

  • [ ] .dump | grep "REFERENCES [a-z_]*_new(" 為空
  • [ ] INSERT INTO query_logs / messages / citation_records 三張都不再報錯
  • [ ] PRAGMA foreign_key_check; 乾淨
  • [ ] 五個索引(查上方列表)已 recreate
  • [ ] Production D1 fk_check 仍乾淨、無資料漂移(待 deploy 後驗證;預期為 no-op)

TD-056 — Workers AI judge 模型 max_completion_tokens: 200 上限被截斷 → JSON parse 失敗 → pipeline_error

Status: open Priority: low high(2026-04-26 重評:TD-061 證明這條是 production 28.6% pipeline_error 的 root cause,不是低頻 cosmetic) Discovered: 2026-04-26 — persist-refusal-and-label-new-chat v0.50.0 production 7.2 verify 抽查 query_logs 時意外撈到。Judge 模型回 JSON 結構包,但 workers_ai_runs_json 顯示 completionTokens: 200 等於上限,content 在 JSON object 中段被截斷,後續 JSON.parse throw → pipeline_error。 Same root cause as: TD-061(acceptance fixture 35 筆 10/10 pipeline_error 全部 completionTokens: 200,見 local/reports/notes/td-061-pipeline-error-investigation-20260426.mdLocation:

  • judge prompt / model call 位置:server/utils/workers-ai.tsserver/utils/judge.ts(或對應 retrieve-then-judge orchestrator;apply 時再精準定位)
  • 截斷觸發 query 樣本:production query_logs.workers_ai_runs_jsoncompletionTokens: 200(精確 query id 待 apply 時撈出)

Related markers: 無 tasks.md marker(本 entry 為發現紀錄;fix 會在獨立 change 處理)

Problem

Workers AI 對 judge 模型呼叫設定 max_completion_tokens: 200,正常 judge JSON 多數在 ~150 tokens 內可結束,但少數 query(候選文件多 / reasoning 較長)會撞 200 token 上限:模型輸出在 JSON 中段被硬截 → 字串非合法 JSON → JSON.parse(...) throw → pipeline 走 refusal 路徑回 pipeline_error

UX 影響有限:v0.50.0 已落地 persist-refusal,使用者會看到 refusal message + reason pipeline_error,不再「整個訊息消失」;但實際上是模型本來能回答、只是被 token 上限切斷。等於把可救援的查詢誤標 refused。

Fix approach

兩條路擇一(apply 時用實機資料 sampling 決定):

  1. 抬高 max_completion_tokens(例如 400 或 512)— 最低成本,但長尾 query 仍可能再撞牆
  2. Judge 模型輸出 schema 改 schema-constrained / structured output(若 Workers AI 該模型支援 JSON mode / response_format)— 強制模型在預算內結束 JSON,超過則 partial 而非 mid-string truncation;搭配 fallback parser 容忍尾段不完整

兩條都要:

  • 加 evlog 在 completionTokens 達 max 時記 wide event field(ex: judge.token_truncated: true),便於後續 production sampling
  • pipeline_error 時保留 raw response snippet(前 N 字元)到 query_logs,方便事後 audit

Acceptance

  • [ ] Fix 部署後立即撈 production query_logs 連續 24-48h 樣本,驗 judge.token_truncated: truepipeline_error 觸發次數收斂到 0(或對照 baseline 顯著下降)—— 取代原「過 7 天觀察」
  • [ ] Local repro:把 judge max_completion 故意調 50 重現截斷 → 修復後 fix path 觸發、不 throw
  • [ ] judge 模型呼叫具備 instrumentation,可區分「真正 refusal」vs「token-budget 截斷」

TD-057 — evlog wide event lifecycle 警告:log.error() / log.set() 在 wide event emit 後呼叫,SSE stream 真實錯誤與結果欄位被吞

Status: open Priority: mid Discovered: 2026-04-26 — production wrangler tail 重複出現兩種同 root cause 的 warning:

[evlog] log.error() called after the wide event was emitted — Keys dropped: operation, error.
[evlog] log.set()   called after the wide event was emitted — Keys dropped: result.

Production live tail 採樣(2026-04-26 21:38-21:40 UTC,6 條 chat)

Warning 類型次數對應 chat 類型
log.error() keys dropped: operation, error3全部對應 D1 refusal_reason='pipeline_error' 的 chat
log.set() keys dropped: result3對應 2 條 no_citation refusal + 1 條成功 chat(first_token=3245ms)

兩種 warning 同 root cause:wide event 在 handler return 時已 emit,SSE ReadableStream.start() 內後續 log.set() / log.error() 都 race 失敗。

Location:

  • server/api/chat.post.ts createSseChatResponseReadableStream.start() callback
  • 上層 wide event 在 SSE response 建立時 emit 完畢;start() 內 stream pipe 若 throw,catch block 試圖 log.error(err, { operation, error }) 已無 owning event,evlog 丟掉 keys

Related markers: 無 tasks.md marker(本 entry 為發現紀錄)

Problem

evlog 設計上一個 request 對應一個 wide event。createSseChatResponse 把 SSE stream 建立後立即 return Response,wide event 在 handler 結束時 emit。SSE 內部的 ReadableStream.start() callback 是異步、長壽命:當 stream 中段 throw(例如 Workers AI fetch 中斷、token decode 失敗),catch handler 呼叫 log.error(err, ...) 時 wide event 已 emit,evlog 紀錄 warn + drop keys。

實際後果:production 看到 pipeline_error 但 wide event 缺 operation / error 細節,無法追真實錯誤;只能靠 wrangler tail 撈當下 console,過了就找不回。直接影響 SSE 路徑可觀察性。

Fix approach

兩條路擇一(apply 時定):

  1. log.fork('sse-stream', fn) 模式 — evlog 若支援 sub-event:在 ReadableStream.start() 內 fork 一個新的 wide event lifecycle,error 寫進 sub-event 而非 parent
  2. 延後 wide event emit — 把 wide event lifecycle 改成跨 stream(handler return 前不 emit,等 stream 自然結束或 abort 才 emit);技術上需 evlog 支援「stream-aware」生命週期,或自寫 wrapper

兩條都需要:

  • 整理當前 createSseChatResponse 的 wide event lifecycle 圖(哪個 emit、何時 emit)寫進 design / docs
  • 確認其他 SSE / streaming endpoint 是否同樣 pattern(avoid one-off fix)

Acceptance

  • [ ] Production wrangler tail 不再出現 [evlog] log.error() / log.set() called after the wide event was emitted warning
  • [ ] SSE stream 中段 error 可在 wide event / sub-event 中看到 operation + error keys
  • [ ] SSE 成功路徑的 result field 也能保留(不再被 lifecycle drop)
  • [ ] 其他 streaming endpoint 不再有相同 lifecycle 漏洞

TD-058 — Production user_profiles 6 條 orphaned rows(profile.id 不在 user.id)

Status: open Priority: low Discovered: 2026-04-26 — TD-053 production 立即驗收時撈表發現 profile_count=23 vs user_count=17Location: production D1 user_profilesRelated markers: 無 tasks.md marker(本 entry 為發現紀錄;fix 會在獨立 change 處理)

Problem

Production user_profiles 表有 6 條 orphaned row:SELECT COUNT(*) FROM user_profiles WHERE id NOT IN (SELECT id FROM user) 回 6。原因:user_profiles.id schema 為 TEXT PRIMARY KEY 而非 REFERENCES user(id),無 FK + 無 cascade,因此 better-auth 自刪 user 時 user_profiles row 不會自動清理;此外 fix-user-profile-id-drift 之前的舊版 session.create.before hook 也可能在 stale row 情境下產生 id 漂移殘留。

非 functional bug(user_profiles 在無對應 user 時不會被任何 hot path 讀到),但會影響:

  1. 報表 / 統計類 query 數字膨脹
  2. UNIQUE email_normalized 假設未來重新註冊同 email 時可能踩到既有 orphaned row

Fix approach

兩階段:

  1. 觀察與分類:撈這 6 條 orphaned row 的 email_normalized / created_at / role_snapshot,比對是否對應 better-auth account.providerId='google' 中已自刪的測試帳號或更早的 schema migration 殘留。判斷是否安全清除。
  2. 清理:寫 one-shot migration 或 SQL script 刪這 6 條(或保留有業務意義者)。一併評估是否將 user_profiles.id 改為 REFERENCES user(id) ON DELETE CASCADE(需謹慎,因為 better-auth 自刪 user 時的順序敏感)。

Acceptance

  • [ ] 6 條 orphaned row 已分類(業務 vs 殘留)並文件化於本 entry
  • [ ] 清理腳本或 migration 落地,production profile_count == user_count(除預期保留)
  • [ ] 評估 user_profiles.id 加 FK 的可行性與 ON DELETE 行為,決策結論寫在此 entry 或獨立 ADR

TD-059 — E2E Tests CI workflow 連續 50+ run 全紅(webServer ESM scheme 'cloudflare:' 不支援)

Status: done Priority: high Discovered: 2026-04-26 — v0.50.1 ship 後驗收時 gh run list --workflow="E2E Tests" 抽 50 條全 failure(最早 2026-04-24 15:52 起,跨 50+ commit、含 v0.49.0 / v0.50.0 / v0.50.1 三次 release) Resolved: 2026-04-26 — 採方案 A(wrangler dev);待 CI run 驗證 3 條綠燈後可關閉 Location:

  • CI workflow .github/workflows/*.yml(E2E Tests job)
  • playwright.config.ts webServer 設定(啟動 nuxt preview
  • .output/server/ build artifact 含 cloudflare: protocol import(typical: cloudflare:workers runtime built-ins)

Related markers: 無 tasks.md marker(本 entry 為發現紀錄;fix 會在獨立 change 處理)

Problem

E2E CI workflow 跑 pnpm test:e2e → Playwright webServer 啟動 nuxt preview 在 Node 22 環境,Node 預設 ESM loader 解析 .output/server/import 'cloudflare:workers'(或類似 cloudflare:* scheme)時 throw:

Error [ERR_UNSUPPORTED_ESM_URL_SCHEME]: Only URLs with a scheme in: file, data, and node are supported by the default ESM loader. Received protocol 'cloudflare:'
    at throwIfUnsupportedURLScheme (node:internal/modules/esm/load:187:11)

webServer process exit 1,Playwright 還沒跑任何 spec 就 fail。E2E 已連紅 1 天以上、50+ commit、3 次 release,等於 ship gate 完全失能 — 任何 e2e regression 都不會被攔截。

Root cause

CI 設 NITRO_PRESET=node-server env var 試圖切換到 Node preset,但 nuxt.config.ts:339 硬編 nitro.preset: 'cloudflare_module' 會勝過 env var → build 出 cloudflare-module artifact(每個 chunk 都 import "cloudflare:workers")。nuxt preview 在 CI 找不到 .output/server/wrangler.json → 回退用 Node 直接執行 → 撞 ESM scheme 不支援。

Resolution(採方案 A)

playwright.config.ts:CI 分支從 nuxt preview 換成

bash
pnpm exec wrangler dev --config .output/server/wrangler.json --port 3010 --ip 127.0.0.1 --persist-to .wrangler/e2e-state --log-level warn

並把 webServer.timeout 拉長到 120s(wrangler dev 首次啟動會 download workerd binary)。

.github/workflows/e2e.yml

  • 移除 NITRO_PRESET=node-server(no-op)、NUXT_HUB_LIBSQL_URLPatch NuxtHub local DB driverBootstrap local sqlite schema(libsql 時代殘留)
  • Build 加 NUXT_KNOWLEDGE_MCP_CONNECTOR_CLIENTS_JSON(baked into runtime config snapshot)
  • 新增 Generate .dev.vars for wrangler dev step(runtime 從 .output/server/.dev.vars 讀 worker env vars)
  • 新增 Bootstrap local D1 schema via wrangler step:跑 15 條 migration 用 wrangler d1 execute DB --local --persist-to .wrangler/e2e-state
  • Run E2E tests step 不再帶 env block(runtime config 改由 .dev.vars 提供)

本機驗證:pnpm exec wrangler dev 2 秒內啟動、HTTP 200、D1 binding 正常。

Trade-offs

  • +對齊 production runtime:workerd 而非 Node,binding(D1 / KV / R2 / AI / Durable Objects)走真實 Cloudflare 路徑
  • +移除 libsql 兩條 patch link:不再需要 patch-hub-db-dev.mjs 的 CI 鉤、NUXT_HUB_LIBSQL_URL env 注入
  • −CI step 數略增(多 Generate .dev.varsBootstrap D1 兩步),但每步 <30s
  • −首次 wrangler dev 啟動 download workerd binary(GitHub runner 上估計 ~5–10s 一次性成本)

Acceptance

  • [x] 本機 pnpm exec wrangler dev 啟動成功並回 200
  • [x] D1 schema bootstrap 全 15 條 migration 跑過(local smoke test)
  • [ ] CI E2E workflow 至少連續 3 條 run 通過(含 main push + tag push)— 待 commit + push 後驗證
  • [x] webServer 啟動 log 不再出現 ERR_UNSUPPORTED_ESM_URL_SCHEME(local smoke 已確認)
  • [x] Fix 的 trade-off(runtime 對齊 / startup 時間 / DB binding 處理)已記錄在本 entry

TD-060 — Production agentic-rag AutoRAG 對 seed acceptance fixture 的 retrieval_score 全低於 directAnswer 門檻

Status: in-progress(實作於 change rag-query-rewriting,staging deploy 後驗收 acceptance evidence) Priority: high Discovered: 2026-04-26 — main-v0.0.54-acceptance run 對 production https://agentic.yudefine.com.tw/mcp 跑 35 筆 seed cases,D1 query_logs 顯示 retrieval_score 平均 0.32–0.44,全部 35 筆都低於 thresholds.directAnswerMin=0.7Location: production agentic-rag AutoRAG index、shared/schemas/knowledge-runtime thresholds、server/utils/knowledge-* retrieval pipeline Related markers: search @followup[TD-060] in repo

Problem

main-v0.0.54-acceptance-latency-run-20260426.md 對 production 跑 35 筆 seed acceptance fixture,query_logs.retrieval_score 統計:

  • mean ~0.36
  • range 0.28–0.44
  • 全部 35 筆 < thresholds.directAnswerMin=0.7
  • 全部 35 筆 < thresholds.judgeMin=0.45

結果:

  • 23 筆 decision_path=no_citation_refuse(治理層保守拒答)
  • 10 筆 decision_path=pipeline_error(重測批次,見 TD-061)
  • 2 筆 decision_path=restricted_blocked(TC-13 / r2,正確 scope 阻擋)

0 筆進入 direct / judge_pass / self_corrected 路徑。

影響:

  • main-v0.0.54-acceptance 報告無法量化「實模型回答品質」(Judge 觸發率、引用正確率、回答正確率)
  • 真實使用者若拿類似中文口語 prompt 問 askKnowledge也會 100% 走治理拒答路徑
  • 治理保險機制本身運作正確(不幻覺),但 RAG 實際可用性 = 0%

可能原因:

  1. AutoRAG 索引 ingest 的文件主題跟 seed prompts 不對應(seed 是 ERP / SOP 主題)
  2. AutoRAG embedding model(@cf/qwen/qwen3-embedding-0.6b)對中文口語匹配能力不足
  3. Chunk 切分策略未讓 score 衝高
  4. thresholds.directAnswerMin=0.7 對 AutoRAG score 分布來說設過高

Fix approach

  1. 檢查 production agentic-rag 索引內容(CF Dashboard 看 ingested 文件主題、筆數、最後 sync)
  2. 抓最近一週 production query_logs 全量 retrieval_score 分布,看 P95;若 P95 都低於 0.7,調整 directAnswerMin 到實際分布合理值
  3. 跑 AutoRAG re-index 用更精細 chunk size(256 tokens)
  4. 若 1-3 都沒救:考慮換 embedding model 或補 reranker

Acceptance

  • [ ] Production query_logs 連續一週新樣本中至少 30% decision_path != no_citation_refuse
  • [ ] 重跑 main-v0.0.54-acceptance 33 筆 fixture,至少 50% 拿到 direct / judge_pass / self_corrected
  • [ ] thresholds 校正決策寫入 docs/decisions/YYYY-MM-DD-rag-thresholds.md

TD-061 — Production query_logs 重測批次 pipeline_error 28.6%(10/35)

Status: open Priority: high Discovered: 2026-04-26 — main-v0.0.54-acceptance run 對 production 跑 50 筆 fixture(33 unique + 17 重測),D1 query_logs.decision_path=pipeline_error 出現 10 次,全部集中在 r2 重測批次 Location: server/utils/knowledge-*server/api/mcp/**、production rate-limit、AutoRAG pipeline Related markers: search @followup[TD-061] in repo

Acceptance dependency:本 TD 的最終 acceptance 驗證依賴 change rag-query-rewriting ramp staging — 必須先讓 fixture 能進 judge gate(即 retrieval_score ≥0.45),才有真實 judge truncation 路徑可 verify TD-061 的 fix 是否消除 pipeline_error。

Problem

對 production 跑 50 筆 fixture:

  • 第 1–35 筆(33 unique seed + 前 2 筆 r2 重測)皆正常返回(accepted / blocked
  • 第 36–50 筆全部觸發 HTTP 429,未進 D1
  • D1 已寫入的 35 筆裡,pipeline_error 10 筆樣本特徵:completion_latency_ms = NULLretrieval_score = NULLjudge_score = NULLrefusal_reason = pipeline_error

10 筆對應的 prompt 包括 TC-05 / TC-06×2 / TC-12 / TC-13 / TC-14 / TC-18×2 / TC-20 / EV-01。

2026-04-26 D1 raw evidence 調查(詳見 local/reports/notes/td-061-pipeline-error-investigation-20260426.md):

  • 10/10 pipeline_error row 的 workers_ai_runs_json 都記錄了唯一一筆 agentJudge@cf/moonshotai/kimi-k2.5),且 completionTokens 全部剛好等於 200(max ceiling)
  • 23 筆 no_citation_refuse 全部 retrieval_score ∈ [0.28, 0.44](< judgeMin=0.45,judge 沒被觸發)
  • 「r2 批次」框架其實是 score-banding 假象 — 真實切割線是 retrieval 是否落入 [judgeMin=0.45, directAnswerMin=0.7) → 進 judge → 100% 觸發 bug

可能原因(按證據強度排序)

  1. HIGH (~85%)server/utils/workers-ai.ts:135 agentJudge max_completion_tokens: 200 對於需要 reformulatedQuery 的 case 不夠 → 截斷 JSON → normalizeStructuredResponse (line 416-418) 直接呼叫 JSON.parse 對截斷字串拋 SyntaxError → web-chat.ts:426 catch block 寫 pipeline_error + 把 retrievalScore 寫為 NULL(line 447)
  2. ~10%:Workers AI runtime 對 response_format: json_schema 的 grammar constraint 支援不完整,回傳非結構化字串
  3. ~5%:cache key collision / pipeline 不冪等(證據不支持:每筆有獨立 judge latency,無共用 run 跡象)

影響:

  • 真實使用者短時間重複問同 prompt 約每 4 個請求就 1 個拿到 pipeline_error
  • 治理層保險仍正常(不幻覺、messages.content_text 不寫原文),但使用者體驗顯示為「服務無回應 / 不穩定」

Fix approach

  1. 驗證假設 1:vitest unit test mock readJudgeResponse 對截斷 JSON 字串(如 '{"shouldA')的行為,確認拋 SyntaxError
  2. 修法(兩條軸線並行)
    • 軸線 A:提高 agentJudge max_completion_tokens 至 512–1024(reformulatedQuery 中文重述 200 token 顯然不夠)
    • 軸線 BreadJudgeResponse 加 truncation guard — 檢查 response.usage.completion_tokens 是否觸頂;若是則明確拋 JudgeTruncationError,catch block 寫 decision_path=judge_truncated
    • 軸線 CnormalizeStructuredResponse 加 try/catch + jsonrepair fallback,部分結構可用就用
  3. pipeline_error enum:至少拆出 judge_error / judge_truncated / retrieval_error / composer_error,schema migration
  4. acceptance fixture 補 retrieval ∈ [0.45, 0.7] 的 case:本批 35 筆都跑不到 judge 區間後又能 succeed,意味 judge 路徑長期沒有 production 監控
  5. 與 TD-057 的關係:TD-057 修不會自動解 TD-061(TD-057 是觀測層警告、TD-061 是功能層 bug,兩者獨立);但 TD-057 修完後 wide event 會帶完整 stack,可作為 TD-061 fix verification 的驗證信號

Acceptance

  • [ ] 找出 10 筆 pipeline_error 的根因類別(至少分 2 類)
  • [ ] 連續 50 筆同 prompt 重測,pipeline_error rate < 5%
  • [ ] decision_path enum 擴張到分類錯誤(schema migration)
  • [ ] Production 連續一週 pipeline_error rate baseline 寫入 docs/verify/

TD-062 — Extract buildRetrieveWithRewriter helper across 3 entry points

Status: open Priority: mid Discovered: 2026-04-26 — /commit 0-A simplify review on change rag-query-rewritingLocation: server/api/chat.post.ts:192-217server/mcp/tools/ask.ts:128-154server/mcp/tools/search.ts:79-106Related markers: search @followup[TD-062] in repo

Problem

三個 entry point 都建立同一形狀的 retrieve closure:判斷 input.useRewriter !== false && isQueryRewritingEnabled(runtimeConfig),組裝同樣 shape 的 rewriteForRetrieval 參數,呼叫 retrieveVerifiedEvidence。約 28 LoC × 3 = 重複面積大;未來要改 rewriter 接口要動三個檔。

Fix approach

buildRetrieveWithRewriter({ runtimeConfig, event, search, store, governance, onRewriterOutcome? }) helper,回傳 (input) => Promise<...> closure。chat.post.ts / mcp/tools/ask.ts 透過 onRewriterOutcome callback 取得 last status / rewrittenQuery 寫進 audit;mcp/tools/search.ts 不需要。helper 放在 server/utils/knowledge-query-rewriter.tsserver/utils/knowledge-retrieval.ts

Acceptance

  • 三個 entry point 各自從 ~28 LoC closure 縮成 ~5 LoC(call helper + optional callback)
  • 既有 unit + integration test 全綠不需改
  • pnpm typecheck 通過

TD-063 — Trim duplicated useRewriter callback docstring

Status: open Priority: low Discovered: 2026-04-26 — /commit 0-A simplify review on change rag-query-rewritingLocation: server/utils/knowledge-answering.ts:65-73server/utils/web-chat.ts:186-193server/utils/mcp-ask.ts:171-180server/utils/mcp-search.ts:14-22Related markers: search @followup[TD-063] in repo

Problem

四處 retrieve callback signature 都各自寫 6-9 行 docstring,重述「retry pass 必須 useRewriter: false 因為 reformulatedQuery 已是 LLM-shaped query」同一段話。違反專案規則「Default to writing no comments」「Don't reference the current task / fix / callers」。

Fix approach

把完整 rationale 留在 server/utils/knowledge-query-rewriter.ts 開頭一份 canonical docstring;四個 callsite 縮成單行 // see knowledge-query-rewriter.ts §S-RW for retry-pass rationale

Acceptance

  • 四個 callsite docstring ≤ 1 行
  • canonical docstring 仍涵蓋 retry pass rationale
  • pnpm typecheck 通過

TD-064 — Integration test mocks DB; should be relocated or replaced with real D1

Status: open Priority: mid Discovered: 2026-04-26 — /commit 0-A code-review on change rag-query-rewritingLocation: test/integration/retrieve-verified-evidence-with-rewriter.spec.tsserver/utils/knowledge-audit.ts:359-400Related markers: search @followup[TD-064] in repo

Problem

test/integration/retrieve-verified-evidence-with-rewriter.spec.tsvi.fn() mock 了 searchresolveCurrentEvidence,沒有真實 D1 / evidence store / auditStore.updateQueryLog round-trip。違反 .claude/rules/testing-anti-patterns.md 「no mocking DB in integration tests」。更關鍵:本 change 風險最高的程式碼之一—— knowledge-audit.ts 的 dynamic SET clause UPDATE—— 沒有任何 test 覆蓋 bind ordering / SQL composition。未來開發者改 setClauses 順序會悄悄壞 audit 寫入。

Fix approach

兩擇一:

  • (a) 把這個 spec 移到 test/unit/,並為 audit dynamic UPDATE 另外寫一個 D1-backed integration test(in-memory better-sqlite3 或 D1 local),assert rewriter_status / rewritten_query 經完整 INSERT → UPDATE → SELECT round-trip 結果正確
  • (b) 保留檔名位置但改寫內容用真實 D1,覆蓋 audit dynamic UPDATE 的 setClauses 順序

Acceptance

  • 至少一個 integration-tier test 對 audit dynamic UPDATE 用真實 D1 round-trip
  • 測試 cover:rewriter_status / rewritten_query 都帶 / 都不帶 / 一個帶一個不帶 三種組合
  • pnpm test 全綠

TD-065 — UpdateQueryLog.rewriterStatus 型別與 NOT NULL 欄位不一致

Status: open Priority: low Discovered: 2026-04-26 — /commit 0-A code-review on change rag-query-rewritingLocation: server/utils/knowledge-audit.ts:357-388Related markers: search @followup[TD-065] in repo

Problem

UpdateQueryLog.rewriterStatus 型別宣告 string | null,但 query_logs.rewriter_status schema 是 TEXT NOT NULL DEFAULT 'disabled'。caller 傳 null 會 bind SQL NULL 進 NOT NULL 欄位 → D1 constraint error 包成 5xx。目前 caller 都從 lastRewriterStatus: string = 'disabled' 起手,永遠不傳 null,是 latent bug。

Fix approach

  1. UpdateQueryLog.rewriterStatus?: string(drop | null
  2. 確認 bindings.push(input.rewriterStatus ?? null) 改成 bindings.push(input.rewriterStatus)(undefined 已被 setRewriterStatus 旗標濾掉)
  3. typecheck 通過

Acceptance

  • 型別不再允許 null
  • typecheck + 既有 test 通過
  • 加一條 unit test:傳 'disabled' / 'success' / 'fallback_timeout' 等都 round-trip 正確

TD-066 — RewriterStatus discrimination 缺 assertNever

Status: open Priority: low Discovered: 2026-04-26 — /commit 0-A code-review on change rag-query-rewritingLocation: server/utils/knowledge-retrieval.ts:137-138Related markers: search @followup[TD-066] in repo

Problem

rewriteResult.status === 'success' ? auditKnowledgeText(...).redactedText : null 是單一 equality check,未走專案規定的 switch + assertNever pattern(見 .claude/rules/development.mdux-completeness.md Exhaustiveness Rule)。將來新增 RewriterStatus 值(如 fallback_blocked)時不會 compiler error。

Fix approach

typescript
import { assertNever } from '~/utils/assert-never'

let rewrittenQueryForAudit: string | null
switch (rewriteResult.status) {
  case 'success':
    rewrittenQueryForAudit = auditKnowledgeText(rewriteResult.rewrittenQuery).redactedText
    break
  case 'fallback_timeout':
  case 'fallback_error':
  case 'fallback_parse':
    rewrittenQueryForAudit = null
    break
  default:
    assertNever(rewriteResult.status, 'retrieveVerifiedEvidence rewriter')
}

Acceptance

  • 新增 RewriterStatus 值(暫測 fallback_blocked)時 typecheck 立刻 fail
  • 移除測試後既有 4 個 status 全綠
  • pnpm audit:ux-drift 不報新漂移

TD-067 — test/tsconfig.json baseline 191 errors

Status: open Priority: mid Discovered: 2026-05-04 — clade v0.3.10 cutover 把 test typecheck 加到 pre-push 階段時揭露 Location: test/tsconfig.json + 63 個 test files Related markers: search @followup[TD-067] in repo

Problem

pnpm exec tsc -p test/tsconfig.json --noEmit 跑出 191 errors in 63 files,分類:

  • Component module not found(最大宗,TS2307)
    • ~/components/chat/MarkdownContent.vue~/components/chat/ConversationHistory.vue~/components/chat/RefusalMessage.vue~/components/auth/DeleteAccountDialog.vue~~/app/components/admin/usage/TimelineChart.vue~~/app/components/debug/OutcomeBreakdown.vue
    • 真實檔案存在,但 test path resolver 找不到(alias / Nuxt auto-import gap,.nuxt/ types 或 test tsconfig paths 沒涵蓋)
  • Mock/fixture type drift(TS2352)
    • acceptance-auth/bindings/fixtures.test.tschat-route-heartbeat.spec.ts 等的 fixture 跟 ChatConversationMessage 型別漂移(後者新增 refusedrefusalReason 欄位)
  • Nitro route key inference 撞「excessive stack depth」(TS2321)
    • create-chat-conversation-history.spec.ts — Nitro 端 type generic 太深
  • legacy-test-roots.test.ts(6,8) TS5097
    • allowImportingTsExtensions: true
  • middleware-admin.test.ts 函式簽名漂移
    • middleware 業務 signature 改成 2 args,test 只給 1
  • 47× TS2345、22× TS18048 跨 mcp/integration tests
  • vitest.config.ts:130,140 TS2345
    • vitest 4.x TestProjectInlineConfiguration.plugins 不接受 unknown[],需顯式 cast 或 import 正確 type
  • test/unit/sse-parser.spec.ts TS2322
    • onBlock callback 隱式 return widening(Promise<string> vs Promise<'continue' | 'terminate'>),需 return 'continue' as const 或顯式 type annotation

clade v0.3.10 把 test-tsconfig 放 pre-push 後立即擋下;clade v0.3.11 把該 check 從中央倉移除(因 5 家 consumer 中有 test/tsconfig.json 的 3 家裡 2/3 baseline 紅,hit rate 過高)。目前不擋 push,但 baseline 仍存在,是真實 type safety gap。

Fix approach

按錯誤類型分批:

  1. Component module not found(先做,最大宗)
    • 確認 path alias(~/@/~~/#shared/)在 test/tsconfig.json paths 設定不全,或 Nuxt auto-import 沒對 test scope 生效
    • test/tsconfig.json paths 或 extends 的 base map
  2. Mock/fixture type drift:grep ChatConversationMessage fixture,補上新增欄位
  3. legacy-test-roots.test.ts:加 allowImportingTsExtensions: true 到 test tsconfig
  4. vitest.config.ts:plugins 加 as PluginOption[] cast,或 import 正確 type
  5. sse-parser.spec.ts:onBlock callback return 'continue' as const 或顯式 annotation
  6. Nitro route key excessive depth:用 as any workaround 或升級 Nitro
  7. middleware-admin.test.ts:找 middleware 真實 signature,補第二個 arg
  8. 47× TS2345 / 22× TS18048:per-file 分析(mock signature drift / Object is possibly undefined 補 narrow)

Acceptance

pnpm exec tsc -p test/tsconfig.json --noEmit 0 errors。完成後可向 clade 中央倉爭取「opt-in 重啟 test-tsconfig pre-push check(hub.json flag)」— 但需先驗證至少 2 家 consumer baseline 也綠。

TD-068 — deploy.yml 兩個 wrangler-action step 缺 secrets: list(違反 cf-workers/secrets.md rule)

Status: open Priority: mid Discovered: 2026-05-09 — clade v0.5.25 新增 rules/modules/runtime/cf-workers/secrets.md 後對 5 consumer 跑 verify checklist 揭露 Location: .github/workflows/deploy.yml — 「Deploy to Cloudflare Workers (production)」(line 156-163)+「Deploy to Cloudflare Workers (staging)」(line 254-261) Related markers: search @followup[TD-068] in repo

Problem

兩個 wrangler-action step 只有 apiToken / accountId / command,沒帶 secrets: | block。意味 worker runtime 用到的 secret(BETTER_AUTH_SECRET / NUXT_SESSION_PASSWORD / ADMIN_EMAIL_ALLOWLIST / MCP_CONNECTOR_CLIENTS_JSON 等)目前不會在每次 deploy 時自動 sync — 必須過去某個 session 手動跑 wrangler secret put 推上去,後續 rotation 容易脫節。

build env 那邊雖然有 ${{ secrets.PROD_BETTER_AUTH_SECRET }} / ${{ secrets.STAGING_BETTER_AUTH_SECRET }} 等,但那是 build-time 注入,build artifact 編進去;runtime secret 仍要走 wrangler secret API(wrangler-action secrets: list 或手動 wrangler secret put)。

違反 clade rule rules/modules/runtime/cf-workers/secrets.md:「Single source of truth = GitHub repo secret;MUST 在 deploy yaml 用 wrangler-action secrets: list 推進 worker;NEVER 手動 wrangler secret put」。

Fix approach

  1. 列出 production worker 跟 staging worker 實際 runtime 需要哪些 secret(pnpm exec wrangler secret list + 比對 server code 內 process.env.X 引用)
  2. 確認 GitHub repo 對應的 PROD_* / STAGING_* Secret 都在
  3. 改 deploy.yml 兩個 step:
    • secrets: | list 列出所有 runtime secret name(worker 端的名稱,不帶前綴)
    • env: block 對應 ${{ secrets.PROD_<NAME> }} / ${{ secrets.STAGING_<NAME> }}
  4. 比對 Notion 是否有 agentic-rag 的 secret 紀錄頁面;無則補
  5. 跑 staging deploy 驗證 secrets: list 自動 sync

Acceptance

  • deploy.yml 兩個 wrangler-action step 都有 secrets: | + env: block
  • staging deploy run log 看到 Found <N> secrets, uploading... 訊息
  • production worker / staging worker wrangler secret list 比對 GitHub Secret 內容一致
  • Notion「GitHub Secrets & 環境變數」(或 agentic-rag 對應 page)有 secret 列表

為什麼登記不立即修

當下 session(2026-05-09 21:30)user 指示「先登記不處理」— agentic-rag 自家 worker 的 secret 拓樸需要時間 audit(多個 build env / runtime secret / NuxtHub bindings 混合),且觸碰 deploy workflow 風險高,要排獨立 session 處理。

TD-069 — T3 evlog 落地 production 缺 D1 evlog_events migration(drain 在 prod 是 dead-write)

Status: open Priority: high — T3 evlog 在 production 形同無作用,所有 wide event drain 都會 silently fail Discovered: 2026-05-10 — clade HANDOFF §2.4 dev smoke 跑 wrangler d1 execute agentic-rag-db --remote --command "SELECT count(*) FROM evlog_events"no such table: evlog_events: SQLITE_ERROR [code: 7500]Location: server/database/migrations/(缺檔)+ nuxt.config.ts @evlog/nuxthub module wire(已 wire 但未生 migration) Related markers: 對應 spectra change archive/...adopt-evlog-nuxthub-ai-t3/ tasks §7.1-7.4 全部 unchecked

Problem

T3 spec apply(commit dbea28d)後 @evlog/nuxthub module 已正確 wire 進 nuxt.config.ts:96-114,119-124,runtime drain plugin 也會在每筆 wide event 時嘗試 INSERT 進 D1 evlog_events table。但 production D1 從來沒這張 table — 因為 T3 apply 時沒跑 pnpm hub:db:migrations:create@evlog/nuxthubhub:db:schema:extend hook 注入的 schema 落成 drizzle migration。

驗證命令:

bash
cd ~/offline/nuxt-edge-agentic-rag
export CLOUDFLARE_API_TOKEN=$(grep -E "^CLOUDFLARE_API_TOKEN=" .env | head -1 | cut -d= -f2-)
npx wrangler d1 execute agentic-rag-db --remote --command "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE '%evlog%'"

回傳:空(沒任何 evlog* table)。

server/database/migrations/ 16 條 SQL(0001 → 0016)grep evlog_events 0 命中。

機制:@evlog/nuxthub setup()nuxt.hook('hub:db:schema:extend', ({ dialect, paths }) => paths.push(resolve2('./runtime/db/schema/events.${dialect}'))) 只負責註冊 schema 到 nuxthub drizzle pipeline;要實際生成 migration 必跑 pnpm hub:db:migrations:create,不是 deploy 時自動執行。

Impact

T3 evlog adoption 表面 audit clean(drain.pipelineWraps=1 / nuxthub.moduleInstalled=1 / enrichers.installed=5,audit script 0/4 block),但實際 production runtime 每筆 wide event drain 都會 INSERT 進不存在的 table → 失敗 → drain.js retry 3 次 → 全敗 → silently swallowed。production 上 0 筆 evlog wide event 被持久化

對應 spectra change tasks §7.1(SELECT count > 0)/ §7.2(event.ai.cost_usd 寫入)/ §7.3(SSE child event 入 D1)/ §7.4(event.actor.id 在 D1 row)全部無法驗。

Fix approach

bash
cd ~/offline/nuxt-edge-agentic-rag
pnpm hub:db:migrations:create
# 應該產生 server/database/migrations/0017_<auto-name>.sql 含 CREATE TABLE evlog_events
ls server/database/migrations/
git diff server/database/migrations/    # 檢查 schema 是否符合 @evlog/nuxthub 預期
git add server/database/migrations/
git commit -m "🐛 fix(evlog): missing D1 migration for evlog_events table (TD-069)"
git push origin main   # staging deploy 自動套 migration
# 或對 production:tag + deploy production 路徑

deploy 完跑:

bash
npx wrangler d1 execute agentic-rag-db --remote --command "SELECT count(*) FROM evlog_events"
# 應回非 0(@evlog/nuxthub drain 開始 INSERT 後)

Acceptance

  • production D1 evlog_events table 存在
  • SELECT count(*) FROM evlog_events WHERE created_at > now() - interval '1 hour' > 0(chat endpoint 觸發後)
  • event.ai.cost_usd / event.ai.tokens / event.ai.tool_calls 在新 row 內可查
  • spectra change adopt-evlog-nuxthub-ai-t3/tasks.md §7.1-7.4 可勾完成

風險

pnpm hub:db:migrations:create 會根據 drizzle schema diff 生 migration。若 nuxthub events.sqlite schema 跟 production D1 已有 schema 有 indirectly conflict(例如 index name 撞),需 review migration SQL 再 commit。不要直接 migrations:create 後盲 push


TD-070 — rag-query-rewriting 人工檢查對齊新 manual-review 規範

Status: open
Priority: mid
Discovered: 2026-05-12 — clade v1.3.6 manual-review.md 新增 Pre-Review Data Readiness + [review:ui] 收斂原則 + verify channel marker schema(散播到 consumer LOCKED 投影)
Location: openspec/changes/rag-query-rewriting/tasks.md ## 人工檢查 section(items 1-7)
Related markers: 此 TD 屬「整批 ingest 工作」追蹤,不在 tasks.md 內標 @followup marker

Problem

rag-query-rewriting## 人工檢查 7 items 未標 marker。按 clade v1.3.6 新規 manual-review.md「Fallback ≠ 允許省略」:所有新寫或 ingest 修改的 items MUST 顯式標 marker。7 items 內容多屬 staging acceptance evidence 抽查(retrieval_score / latency p95 / fallback rate / 抽 query_log_debug 記錄 / production safety check / Decision Q1 / Decision Q2)→ 該全部標 [discuss](Claude 主導 evidence 收集 + user walkthrough 拍板)。

User 決定本次 session 登記不處理(2026-05-12 對話中明示「agentic-rag 全部都登記 不處理」)。

Fix approach

對 7 items 各加 [discuss] marker(schema: - [ ] #N [discuss] <description> @no-screenshot if applicable)。逐項 evidence 在 spectra-archive Step 2.5 Discuss Walkthrough 流程由 Claude 準備、user 拍板 OK 後寫 (claude-discussed: <ISO>) annotation。Items 1-7 對應動作:

  • #1-#3:retrieval_score / latency / fallback rate — 對應 task §6.5 acceptance evidence
  • #4:抽 3 條 staging query_log_debug 記錄人工判斷改寫合理性
  • #5:production safety check(features.queryRewriting=false)
  • #6-#7:Decision Q1/Q2 商業判斷

Acceptance

  • 7 items 都有 [discuss] marker
  • archive-gate.sh Check 4 驗 [discuss] items 都有 evidence trail 或勾選
  • spectra-archive 可通過 manual-review hygiene gate

TD-071 — AutoRAG → AI Search API migration

Status: done Priority: critical Resolved: 2026-06-09 — v0.57.1 production deploy 成功(CI run 27185472621 全綠;staging gate + production deploy + smoke test 通過) Discovered: 2026-06-09 — rag-query-rewriting staging acceptance run 全掛(evlog: AutoRAGInternalError: vectorize_filter_not_serializable → fix filter shape → Invalid inputLocation: server/utils/ai-search.ts(binding + request shape)、server/utils/knowledge-retrieval.ts(filter construction) Related markers: search @followup[TD-071] in repo Blocks: rag-query-rewriting 6.3-6.6 staging acceptance(retrieval pipeline 全掛,撈不到 retrieval_score)

Problem

Cloudflare 已將 AutoRAG 遷移至 AI Search。舊版 env.AI.autorag(indexName).search({query, filters, max_num_results, ranking_options, rewrite_query}) 在 staging 從 2026-05-04 起 100% 500。

兩階段 root cause(evlog ground truth):

  1. Filter shape(已 fix v0.56.6):{type:'eq', key, value} compound filter 被 Vectorize 拒絕 → vectorize_filter_not_serializable
  2. Search request shape(已 fix v0.56.7 移除 filter,但仍 500):AutoRAG .search() 即使傳空 filter 也報 Invalid input — 整個 request payload shape(max_num_results / ranking_options / rewrite_query)不再被接受

新 API 形式(per Cloudflare docs 2026-06):

  • Binding: env.AI_SEARCH.get(instanceId) 而非 env.AI.autorag(indexName)
  • Request: {query, ai_search_options: {retrieval: {max_num_results, match_threshold, filters}}}
  • Filter: {and: [{eq: {"metadata.category": "..."}}]} 或 implicit key-value {folder: "..."}
  • Response: chunks array 而非 data array

Fix approach

  1. [x] 改 ai-search.ts:binding 對象、request shape、response parsing,使用 AI_SEARCH.get(instanceId).search()
  2. [x] 改 knowledge-retrieval.ts:不再輸出 legacy AutoRAG filter shape;D1 post-search verification 仍是權限真相
  3. [x] 改 Web chat / MCP ask / MCP search callsite 與 accessor:AI 保留給 Workers AI .run(),search 改 AI_SEARCH
  4. [x] 改 wrangler/deploy/render config:production/staging 同時保留 AIAI_SEARCH
  5. [x] 改 unit/integration/acceptance fakes(mock AutoRAG binding → mock AI Search namespace binding)
  6. [x] staging redeploy + 驗證 chat 200 + retrieval_score 有值(2026-06-09 deploy Version 8a518f2b;D1 evidence: retrieval_score=0.51
  7. [x] production deploy v0.57.1(2026-06-09 CI run 27185472621;staging gate + deploy-production + smoke-test 全綠)

Current evidence

  • Local unit gate: 3 passed / 21 tests for ai-search, knowledge-retrieval, require-ai-binding
  • Integration subset: 5 passed / 27 tests for chat route, MCP ask/search, MCP route/access mocks
  • Full local gate: pnpm typecheck no errors; pnpm build complete; test:unit 128 passed / 806 tests; test:integration 90 passed / 485 passed / 1 skipped
  • Static audit: no .autorag(, method: 'autorag', or response.data hits in server test wrangler*.jsonc .github/workflows/deploy.yml; remaining legacy-shaped keys are internal adapter input and tests asserting translation
  • Wrangler rendered staging dry-run: .output/server/wrangler.staging.json includes env.AI_SEARCH (default) AI Search Namespace, env.AI AI, and NUXT_KNOWLEDGE_AI_SEARCH_INDEX ("agentic-rag-staging")
  • Staging deploy (2026-06-09): Version 8a518f2b-3146-4e60-8756-b51b28819b81env.AI_SEARCH (inherited) AI Search Namespace confirmed
  • Staging D1 evidence: query_log 2508e06c at 2026-06-08T20:25:38Zdecision_path=judge_pass_refuseretrieval_score=0.51(AI Search 成功回傳 score)
  • MCP SSE acceptance: 4/5 pass(initialize / notifications / tools/list / SSE channel ✅;askKnowledge fail 根因是 rewriter_status=fallback_error,屬 rag-query-rewriting scope)
  • Production deploy (2026-06-09): v0.57.1 tag push 觸發 deploy workflow run 27185472621;verify-ci-gate ✅ → verify-staging-gate ✅ → deploy-production ✅ → smoke-test ✅ → notify ✅。curl https://agentic.yudefine.com.tw HTTP 200

Acceptance

  • Staging /api/chat 回 200(不再 500)
  • query_logs.retrieval_score 有非零值
  • query_logs.rewriter_status 正確寫入(disabled / success / fallback_*)
  • Local unit/integration gates 全綠(806 unit tests passed;485 integration tests passed / 1 skipped)
  • rag-query-rewriting acceptance 6.4-6.5 可跑出 retrieval_score 分布

Docs powered by VitePress