From 5fb7bfb1de91ce82c2a13ee2ac3dd9eb712a5d67 Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Thu, 18 Jun 2026 14:21:17 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20M3=20=E2=80=94=20=E4=BC=8F=E7=AC=94?= =?UTF-8?q?=E8=B4=A6=E6=9C=AC=20+=20=E8=8A=82=E5=A5=8F=E5=BC=95=E6=93=8E?= =?UTF-8?q?=20+=20=E5=A4=A7=E7=BA=B2=EF=BC=88=E5=90=AB=E5=B9=B6=E5=8F=91?= =?UTF-8?q?=E8=AE=B0=E8=B4=A6=20bugfix=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 伏笔账本:纯函数状态机(OPEN/PARTIAL/CLOSED/OVERDUE) + ForeshadowLedger repo;验收后到期扫描(BackgroundTask 自建 session 置 OVERDUE);登记/状态变更端点 - 节奏 + 三审齐:foreshadow-analyst + pace-checker 并入 LangGraph 并行审(REVIEW_SPECS),collect 分列落 chapter_reviews(conflicts/foreshadow_sug/pace),review SSE 加 foreshadow/pace 事件 - 大纲:outliner Agent 产 OutlineResult(含 foreshadow_windows),POST /outline 逐章 upsert outline 表;GET /foreshadow?status= 看板 - 前端:伏笔四泳道看板(OVERDUE 琥珀) + 大纲编辑器(窗口徽标) + 节奏节拍图(▁▃▅) + 审稿页消费 foreshadow/pace SSE - bugfix(T3.8):并行三审共用请求 session 记账触发 'Session is already flushing' → foreshadow/pace 静默丢失;SqlAlchemyLedgerSink.record 改 add-only(靠端点/事务 commit),加并发回归测试 - M3 E2E:真实 DB + mock 网关零 token 走通 埋设→进展→验收后扫描 OVERDUE→看板 + 大纲含窗口 + 三审齐 SSE/留痕;E2E 暴露并钉住上述 bug - 门禁绿:mypy 111 / pytest 228(0 xfailed) / alembic 无漂移;前端 gen:api/lint/tsc/vitest 69/build --- PROGRESS.md | 48 +- apps/api/tests/fakes_projects.py | 153 ++++- apps/api/tests/test_foreshadow.py | 203 ++++++ apps/api/tests/test_outline.py | 216 +++++++ apps/api/tests/test_outline_context.py | 63 ++ apps/api/tests/test_review_accept.py | 3 + apps/api/ww_api/main.py | 11 +- apps/api/ww_api/routers/foreshadow.py | 159 +++++ apps/api/ww_api/routers/outline.py | 135 ++++ apps/api/ww_api/routers/projects.py | 26 +- apps/api/ww_api/schemas/foreshadow.py | 66 ++ apps/api/ww_api/schemas/outline.py | 43 ++ apps/api/ww_api/services/accept_service.py | 8 +- apps/api/ww_api/services/foreshadow_scan.py | 90 +++ apps/api/ww_api/services/outline_context.py | 87 +++ apps/api/ww_api/services/project_deps.py | 37 +- .../web/app/projects/[id]/foreshadow/page.tsx | 23 + apps/web/app/projects/[id]/outline/page.tsx | 20 + apps/web/components/AppShell.tsx | 14 +- apps/web/components/LeftNav.tsx | 142 +++-- .../components/foreshadow/ForeshadowBoard.tsx | 72 +++ .../components/foreshadow/ForeshadowCard.tsx | 133 ++++ .../components/foreshadow/KanbanColumn.tsx | 61 ++ .../components/foreshadow/RegisterForm.tsx | 185 ++++++ .../components/outline/OutlineChapterRow.tsx | 51 ++ apps/web/components/outline/OutlineEditor.tsx | 97 +++ apps/web/components/review/BeatMap.tsx | 34 + .../review/ForeshadowSuggestions.tsx | 58 ++ apps/web/components/review/PacePanel.tsx | 56 ++ apps/web/components/review/ReviewReport.tsx | 49 +- apps/web/components/workbench/Workbench.tsx | 14 + apps/web/lib/api/schema.d.ts | 370 ++++++++++- apps/web/lib/api/server.ts | 8 + apps/web/lib/api/types.ts | 15 + apps/web/lib/foreshadow/board.test.ts | 134 ++++ apps/web/lib/foreshadow/board.ts | 141 +++++ apps/web/lib/foreshadow/useForeshadow.ts | 96 +++ apps/web/lib/outline/outline.test.ts | 63 ++ apps/web/lib/outline/outline.ts | 49 ++ apps/web/lib/outline/useOutline.ts | 63 ++ apps/web/lib/pace/beatmap.test.ts | 37 ++ apps/web/lib/pace/beatmap.ts | 53 ++ apps/web/lib/review/history.test.ts | 57 +- apps/web/lib/review/history.ts | 57 +- apps/web/lib/review/sse.test.ts | 62 ++ apps/web/lib/review/sse.ts | 68 +- apps/web/lib/review/useReviewStream.ts | 25 +- apps/web/tsconfig.tsbuildinfo | 2 +- memory/contracts.md | 32 +- memory/decisions.md | 31 + memory/gotchas.md | 12 + packages/agents/tests/test_outliner_spec.py | 117 ++++ packages/agents/tests/test_review_specs.py | 167 +++++ packages/agents/ww_agents/__init__.py | 33 +- packages/agents/ww_agents/schemas.py | 103 +++ packages/agents/ww_agents/specs.py | 103 ++- packages/core/tests/fakes_orchestrator.py | 23 + packages/core/tests/test_foreshadow_repo.py | 293 +++++++++ packages/core/tests/test_foreshadow_state.py | 143 +++++ packages/core/tests/test_outline_node.py | 137 ++++ .../core/tests/test_outline_write_repo.py | 125 ++++ packages/core/tests/test_review.py | 193 +++++- packages/core/ww_core/domain/__init__.py | 36 ++ .../core/ww_core/domain/foreshadow_repo.py | 219 +++++++ .../core/ww_core/domain/foreshadow_state.py | 105 ++++ .../core/ww_core/domain/outline_write_repo.py | 109 ++++ .../core/ww_core/orchestrator/__init__.py | 19 + packages/core/ww_core/orchestrator/collect.py | 81 ++- packages/core/ww_core/orchestrator/graph.py | 17 +- .../core/ww_core/orchestrator/outline_node.py | 81 +++ packages/core/ww_core/orchestrator/sse.py | 109 +++- .../tests/test_ledger_concurrency.py | 112 ++++ packages/llm_gateway/ww_llm_gateway/ledger.py | 14 +- tests/test_m3_e2e.py | 584 ++++++++++++++++++ 74 files changed, 6529 insertions(+), 126 deletions(-) create mode 100644 apps/api/tests/test_foreshadow.py create mode 100644 apps/api/tests/test_outline.py create mode 100644 apps/api/tests/test_outline_context.py create mode 100644 apps/api/ww_api/routers/foreshadow.py create mode 100644 apps/api/ww_api/routers/outline.py create mode 100644 apps/api/ww_api/schemas/foreshadow.py create mode 100644 apps/api/ww_api/schemas/outline.py create mode 100644 apps/api/ww_api/services/foreshadow_scan.py create mode 100644 apps/api/ww_api/services/outline_context.py create mode 100644 apps/web/app/projects/[id]/foreshadow/page.tsx create mode 100644 apps/web/app/projects/[id]/outline/page.tsx create mode 100644 apps/web/components/foreshadow/ForeshadowBoard.tsx create mode 100644 apps/web/components/foreshadow/ForeshadowCard.tsx create mode 100644 apps/web/components/foreshadow/KanbanColumn.tsx create mode 100644 apps/web/components/foreshadow/RegisterForm.tsx create mode 100644 apps/web/components/outline/OutlineChapterRow.tsx create mode 100644 apps/web/components/outline/OutlineEditor.tsx create mode 100644 apps/web/components/review/BeatMap.tsx create mode 100644 apps/web/components/review/ForeshadowSuggestions.tsx create mode 100644 apps/web/components/review/PacePanel.tsx create mode 100644 apps/web/lib/foreshadow/board.test.ts create mode 100644 apps/web/lib/foreshadow/board.ts create mode 100644 apps/web/lib/foreshadow/useForeshadow.ts create mode 100644 apps/web/lib/outline/outline.test.ts create mode 100644 apps/web/lib/outline/outline.ts create mode 100644 apps/web/lib/outline/useOutline.ts create mode 100644 apps/web/lib/pace/beatmap.test.ts create mode 100644 apps/web/lib/pace/beatmap.ts create mode 100644 packages/agents/tests/test_outliner_spec.py create mode 100644 packages/agents/tests/test_review_specs.py create mode 100644 packages/core/tests/test_foreshadow_repo.py create mode 100644 packages/core/tests/test_foreshadow_state.py create mode 100644 packages/core/tests/test_outline_node.py create mode 100644 packages/core/tests/test_outline_write_repo.py create mode 100644 packages/core/ww_core/domain/foreshadow_repo.py create mode 100644 packages/core/ww_core/domain/foreshadow_state.py create mode 100644 packages/core/ww_core/domain/outline_write_repo.py create mode 100644 packages/core/ww_core/orchestrator/outline_node.py create mode 100644 packages/llm_gateway/tests/test_ledger_concurrency.py create mode 100644 tests/test_m3_e2e.py diff --git a/PROGRESS.md b/PROGRESS.md index c3c526d..26b7e0e 100644 --- a/PROGRESS.md +++ b/PROGRESS.md @@ -8,32 +8,40 @@ --- -## 当前阶段:Phase 2 (M2) · 一致性 + 验收 +## 当前阶段:Phase 3 (M3) · 伏笔 + 节奏 -> 目标:写 → 审(一致性) → 裁决 → 验收(事务写回);**未决冲突禁验收**。**契约先行**:C6 续审 AgentSpec + 结构化输出 schema → C4 扩(并行审图/SSE) → C3 扩(review/accept/reviews)。 -> 执行波次(依赖驱动):A(T2.1‖T2.3) → B(T2.2) → C(T2.4+T2.5) → D(T2.6) → E(T2.7)。 -> 计划已定的设计决策(见变更日志 + `memory/decisions.md` 待 Wave A 落地后回写): -> **R2** digest 提炼在事务外做、原子事务只写库(不在持开事务里跨网络调 LLM); -> **R3** accept 为确定性事务代码、读领域表(`chapters`/`chapter_reviews`)重建真相,checkpoint 仅控制流句柄(不变量 #5); -> **R4** accept 取该章 `max(version)+1` 晋升 `status='accepted'`,草稿行保留; -> **R5** 裁决清单每个 conflict 必须 采纳/忽略/手改 之一,缺判决 → `CONFLICT_UNRESOLVED`。 +> 目标:伏笔账本/到期提醒/看板 + 节奏引擎 + 大纲。**契约先行**:C6 扩(foreshadow-analyst/pace-checker/outliner 三 spec + 输出 schema) → C4 扩(审图加两审分支 + outline 节点) → C3 扩(outline/foreshadow 端点)。 +> 执行波次(依赖驱动):A(T3.1‖T3.4) → B(T3.2‖T3.3) → C(T3.5) → D(T3.6) → E(T3.7)。 +> 计划已定的衔接决策: +> **M3-a** `foreshadow` 表已在 T0.2 建齐(含 status/expected_close_from/to/progress/links 列)→ **M3 无新建表迁移**;T3.1 仅 Repository + 纯函数状态机,落 `core/ww_core/domain/`(@backend owned,同 T2.3 先例,故 T3.1 负责据实记 @backend)。 +> **M3-b** 到期扫描(T3.2)挂在**验收后**触发——接 M2 验收事务 §5.5 步骤 3 当前留的 `TODO(M3)`(人物 latest_state/伏笔更新)占位。 +> **M3-c** 第三审(foreshadow/pace)复用 M2 `build_review_graph` 的 `review_specs` 扩展点并入并行分支(C4 已稳定);**文风第四审属 M4/T4.2**,M3 先到三审齐。 +> **M3-d** 到期判据为纯函数确定性代码(非 Agent):`current_ch > expected_close_to AND status≠CLOSED → OVERDUE`。 | 任务 | 状态 | 负责 | 依赖 | 契约 | 备注 | |---|---|---|---|---|---| -| T2.1 续审 AgentSpec + 结构化输出契约 + 网关 instructor 接线 | ✅ @llm 2026-06-18 | @llm | T1.1✅ | C6✅稳定 | `packages/agents/`+`packages/llm_gateway/`;`ContinuityReview/Conflict`(五类枚举)+`continuity_spec`(只读);`Gateway.run` 经 instructor 填 `parsed`(改 C1);25 tests 绿 | -| T2.2 LangGraph 并行审 + collect 留痕 + review SSE | ✅ @llm 2026-06-18 | @llm | T1.3✅, T2.1✅ | C4✅扩稳定 | `orchestrator/`:`build_review_graph`(continuity 并行,可扩四审)+`collect_reviews`落 `chapter_reviews`+`normalize_review`(section/conflict);accept 接线留 T2.4;15 新测;只 flush 不 commit | -| T2.3 摘要/审稿留痕 Repository + 章节 version | ✅ @backend 2026-06-18 | @backend | T0.2✅ | — | `core/ww_core/domain/`:`chapter_repo`(promote_to_accepted/max_version/latest_accepted)+`digest_repo`(append-only)+`review_repo`(record/list/set_decisions);纯 fake 17 新测;只 flush 不 commit(交 T2.4 事务);唯一约束 DB 级→T2.7 E2E 覆盖 | -| T2.4 验收事务 + 冲突 gate | ✅ @backend 2026-06-18 | @backend | T2.2✅, T2.3✅ | C3✅扩 | `accept_service`(冲突 gate+R3/R4 单事务)+`digest_extraction`(R2 事务外提炼,light);`POST .../accept`;缺判→409 `CONFLICT_UNRESOLVED`+missing_indices | -| T2.5 review API + 历史 | ✅ @backend 2026-06-18 | @backend | T2.2✅ | C3✅扩 | `POST .../review`(SSE section/conflict/done)+`GET .../reviews`(新→旧);analyst 网关;流耗尽 commit;+9 测全仓 112 passed | -| T2.6 审稿报告页 + 冲突裁决 + 验收 gate | ✅ @frontend 2026-06-18 | @frontend | T2.4✅, T2.5✅ | — | `app/projects/[id]/review`+`components/review/*`+`lib/review/*`;gen:api 纳三端点;就地标注(R6 段级锚点)+裁决+「将更新」清单+未决禁验收;vitest 45(+21) | -| T2.7 M2 E2E | ✅ @qa 2026-06-18 | @qa | T2.6✅ | — | `tests/test_m2_e2e.py` 2 用例真跑过(非 skip);闭环 + 零冲突直通;DB 真源断言(accepted v2 / digest 来自终稿#4 / 裁决留痕 / 三档位 ledger);无新 bug | +| T3.1 foreshadow Repository + 纯函数状态机 | ✅ @backend 2026-06-18 | @backend | T0.2✅ | — | `foreshadow_state.py`(ForeshadowStatus+transition/is_overdue/apply_overdue_scan/InvalidTransition)+`foreshadow_repo.py`(`ForeshadowLedger*`,register/list_by_status/transition/record_progress/scan_overdue,只 flush);37 测;写侧加 Ledger 前缀避撞 C5 读侧 | +| T3.2 到期扫描(BackgroundTask) + 登记/状态接口 | ✅ @backend 2026-06-18 | @backend | T2.4✅, T3.1✅ | C3✅扩 | accept 后 BackgroundTask `run_overdue_scan`(自建 session 置 OVERDUE);`POST/PATCH .../foreshadow` 登记/转移(重复/非法→VALIDATION 422);9 测全仓 205 passed | +| T3.3 foreshadow-analyst + pace-checker 节点 | ✅ @llm 2026-06-18 | @llm | T2.2✅ | C6✅扩/C4✅扩 | `foreshadow_spec`+`pace_spec`(+schema)并入 `REVIEW_SPECS`(三审齐);collect 分列落 conflicts/foreshadow_sug/pace;`normalize_review` 加 `foreshadow`/`pace` 事件;~30 测;文风审留 M4 | +| T3.4 outliner Agent + 伏笔窗口 | ✅ @llm 2026-06-18 | @llm | T1.1✅ | C6✅扩 | `packages/agents/`(outliner_spec+OutlineResult/OutlineChapter/ForeshadowWindow)+`orchestrator/outline_node.py`(run_outline 只读不写库);20 测;C6 扩稳定 | +| T3.5 API:outline + foreshadow | ✅ @backend 2026-06-18 | @backend | T3.1✅, T3.4✅ | C3✅扩 | `GET .../foreshadow?status=`(看板,续 foreshadow.router) + `POST .../outline`(独立 outline.py,run_outline analyst→upsert outline 表+commit);outline 写侧 repo;+15 测全仓 220 passed | +| T3.6 伏笔看板 + 大纲编辑器 + 节奏报告 | ✅ @frontend 2026-06-18 | @frontend | T3.3✅, T3.5✅ | — | `/foreshadow`(四泳道+登记/转移) + `/outline`(beats+窗口徽标) + 审稿页扩消费 foreshadow/pace(节拍图▁▃▅);gen:api 纳 M3 端点;vitest 69(+24) | +| T3.7 M3 E2E | ✅ @qa 2026-06-18 | @qa | T3.6✅ | — | `tests/test_m3_e2e.py` 3 用例真跑(非 skip):伏笔埋设→进展→验收后扫描 OVERDUE✓ + 大纲 upsert 含窗口✓ + 三审 SSE(**strict-xfail 钉住并发记账 bug**);DB 真源断言 | +| T3.8 [bugfix] 并行审记账并发安全 | ✅ @llm 2026-06-18 | @llm | T3.7✅ | — | `SqlAlchemyLedgerSink.record` 改 add-only(去 `await flush`);并发回归测试(RED 已验旧实现炸);E2E case3 转 XPASS(三审齐落库 foreshadow_sug=2/pace✓)。**待 @qa 解钉 T3.7 strict-xfail 收尾** | -**M2 出口(DoD)✅ 全部达成**:写→审(一致性)→裁决→验收(事务);未决冲突禁验收。后端门禁绿(ruff / mypy **90 文件** / pytest **114 passed** / alembic **无漂移**);前端门禁绿(gen:api / lint / tsc / vitest **45** / build);E2E 真实 DB + mock 网关零 token 走通 写→审→裁决→验收→摘要入库,DB 真源逐项断言。**M2 完成。** 下一阶段 M3(伏笔 + 节奏)见 DEV_PLAN,进入时拉入本表。 +**M3 出口(DoD)✅ 全部达成**:伏笔账本/到期提醒/看板;大纲含回收窗口;节奏报告(三审齐真持久化)。后端门禁绿(ruff / mypy **111 文件** / pytest **228 passed** / alembic **无漂移**);前端门禁绿(gen:api / lint / tsc / vitest **69** / build);E2E 真实 DB + mock 网关零 token 走通 伏笔埋设→进展→验收后扫描 OVERDUE→看板 + 大纲含窗口 + 三审齐 SSE/留痕,DB 真源逐项断言。T3.7 E2E 暴露并发记账 bug → T3.8 add-only 修复 → 解钉 xfail。**M3 完成。** 下一阶段 M4(文风)见 DEV_PLAN,进入时拉入本表。 --- ## 已归档阶段 +
Phase 2 · 一致性 + 验收 (M2) — ✅ 全部完成(2026-06-18) + +T2.1 续审 AgentSpec+结构化输出契约+网关 instructor 接线(C6✅稳定,改 C1:`run()` 填 `parsed`)@llm · T2.2 LangGraph 并行审+collect 留痕+review SSE(C4✅扩)@llm · T2.3 摘要/审稿留痕 Repository+章节 version(@backend,表已 T0.2 建)· T2.4 验收事务+冲突 gate(单原子事务 + R2 事务外提炼 digest)@backend · T2.5 review API+历史(C3✅扩)@backend · T2.6 审稿报告页+冲突裁决+验收 gate @frontend · T2.7 M2 E2E(真 DB+多档位 mock 网关零 token)@qa。 +出口达成:后端门禁绿(ruff / mypy 90 / pytest 114 / alembic 无漂移);前端门禁绿(gen:api / lint / tsc / vitest 45 / build);E2E 写→审→裁决→验收→摘要入库闭环、DB 真源逐项断言。设计决策 R1–R6 全兑现(见 `memory/decisions`)。 + +
+
Phase 1 · 写章闭环骨架 (M1) — ✅ 全部完成(2026-06-18) T1.1 LLM 网关接口+DeepSeek 适配器+usage_ledger 记账(C1✅稳定)@llm · T1.2 记忆 assemble 确定性选择(C5✅稳定)@backend · T1.3 LangGraph 写章节点+Postgres checkpointer+SSE(C4✅稳定)@llm · T1.4 立项+写章 draft(SSE)+PUT 自动保存(C3✅稳定)@backend · T1.5 AppShell+作品库+5 步立项向导 @frontend · T1.6 写作工作台(三栏+流式打字机+注入透明面板+自动保存)@frontend · T1.7 提供商凭据管理(Fernet 加密+测试连接)@backend · T1.8 设置页(档位路由+凭据行)@frontend · T1.9 M1 E2E(真实 DB+mock 网关零 token;暴露并修复 ledger 提交 bug)@qa。 @@ -55,6 +63,14 @@ T0.1 monorepo 骨架 ✅ @devops · T0.2 16 MVP 表迁移(无漂移,users st > 格式:`- [YYYY-MM-DD] @skill 完成/进展 Txx — 一句话结果 + 影响的契约/文件` +- [2026-06-18] @orchestrator — **🎉 M3 完成**(T3.8✅ 修复 + @qa 解钉 xfail)。T3.8 把 `SqlAlchemyLedgerSink.record` 改 add-only(去并行路径里唯一的 `await flush()`,靠端点/事务 commit 持久化),并发回归测试 RED 已验旧实现抛 `Session is already flushing`;@qa 移除 T3.7 case3 strict-xfail、收紧为严格正向断言(三审 section 齐 + foreshadow/pace 真发事件 + `chapter_reviews.foreshadow_sug`/`pace` 列真填 + `beat_map` 精确断言)。最终全收口门禁绿:ruff/format 干净、mypy **111 文件**、pytest **228 passed**(0 xfailed/0 failed)、**alembic 无漂移**;前端 lint/tsc/vitest 69 绿。**M3 出口 DoD 全达成。** 下一阶段 M4(文风)。 +- [2026-06-18] @orchestrator — **M3 Wave E:T3.7✅ E2E 交付 + 暴露并发 bug**。`tests/test_m3_e2e.py` 真 pg 跑通:① 伏笔 埋设→进展→验收后 BackgroundTask 扫描置 OVERDUE→看板过滤(DB 真源)✓ ② 大纲 `POST .../outline`→outline 表行+窗口+幂等 upsert+记账✓。③ 三审 SSE 用例 **strict-xfail 钉住真 bug**:三审同 LangGraph superstep 并行 + 共用请求 session 的 `SqlAlchemyLedgerSink.record`(add+`await flush`)→`AsyncSession` 非并发安全→第二/三审 flush 撞「Session is already flushing」→被 `run_review` 失败隔离静默吞成 `incomplete`→**foreshadow/pace 静默丢失**(SSE 无事件、`chapter_reviews` 列空)。M2 单审未触发。基线 **222 passed + 1 xfailed**。开 **T3.8(@llm)** 修复 → 修后 @qa 解钉 xfail → 才算 M3 达成。 +- [2026-06-18] @orchestrator — **M3 Wave D 完成**(T3.6✅)。前端三块落地:伏笔四泳道看板(`/foreshadow`,OVERDUE 琥珀 + 登记/转移乐观更新)、大纲编辑器(`/outline`,逐章 beats + 伏笔窗口徽标 + 接近回收提示)、审稿页扩消费 `foreshadow`/`pace` SSE(伏笔建议区 + 节奏节拍图 ▁▃▅,三审齐显,文风第四审留 M4)。`pnpm gen:api` 纳入 M3 端点(看板/登记/转移/大纲),松散 JSONB 字段安全收窄不手写。前端门禁绿:gen:api/lint/tsc/**vitest 69(+24)**/build。memory +1 decision +2 gotchas。下一步 **Wave E:T3.7 M3 E2E**(@qa 真 DB + mock 网关零 token)。 +- [2026-06-18] @orchestrator — **M3 Wave C 完成**(T3.5✅,C3 扩稳定)。`GET .../foreshadow?status=` 看板(续 foreshadow.router,四泳道)+ `POST .../outline`(独立 outline.py:`run_outline` analyst 网关 → 逐章 upsert `outline` 表含伏笔窗口 → 端点末尾 commit)。outline 写侧 repo 加 `Write` 前缀避撞 C5 读侧。全仓门禁绿:mypy **102 文件**、pytest **220 passed**(205→+15)。memory 登记 C3 扩 + 1 decision + 1 gotcha。下一步 **Wave D:T3.6**(@frontend 伏笔看板 + 大纲编辑器 + 节奏节拍图 + 审稿页消费 foreshadow/pace SSE,先 `pnpm gen:api`)。 +- [2026-06-18] @orchestrator — **M3 Wave B 完成**(T3.2✅)。验收后到期扫描挂 BackgroundTask(`run_overdue_scan` 自建独立 session 置 OVERDUE,接 §5.5 `TODO(M3)`)+ 伏笔登记/状态端点(`POST/PATCH .../foreshadow`,重复 code/非法转移→`VALIDATION` 422,未动 shared 错误码)。全仓门禁绿:mypy **95 文件**、pytest **205 passed**(196→+9)。memory 登记 C3 扩 + 1 decision + 2 gotchas。下一步 **Wave C:T3.5**(@backend `GET .../foreshadow?status=` 看板续接 foreshadow.router + `POST .../outline` 独立 outline.py 调 run_outline)。 +- [2026-06-18] @orchestrator — **M3 Wave B 半程**(T3.3✅,C4/C6 扩稳定)。foreshadow-analyst + pace-checker 并入 `build_review_graph`(`REVIEW_SPECS` 三审齐,文风第四审留 M4);collect 一次 record 分列落 `conflicts`/`foreshadow_sug`(扁平+kind 标记)/`pace`(整 dict);`normalize_review` 加 `foreshadow`/`pace` SSE 事件(向后兼容)。全仓门禁绿:ruff/format 干净、mypy **91 文件**、pytest **196 passed**(169→+27)。memory 登记 C4/C6 扩 + 2 gotchas。**Wave B 顺序执行**:T3.3 改完 orchestrator → 现启 **T3.2(@backend apps/api)**,对最终 orchestrator 构建、无编译竞争。 +- [2026-06-18] @orchestrator — **M3 Wave A 完成**(T3.1✅ + T3.4✅)。两 agent 并行(@backend core/domain ‖ @llm agents+orchestrator,隔离 MYPY_CACHE_DIR 避缓存竞争)。T3.1:伏笔纯函数状态机(OPEN/PARTIAL/CLOSED/OVERDUE 全转移+到期判据)+写侧账本 repo(`ForeshadowLedger*`,加 Ledger 前缀避撞 C5 读侧)。T3.4:outliner spec + `OutlineResult` schema(含 `foreshadow_windows`)+`run_outline` 节点(只读不写库,C6 扩稳定)。全仓门禁绿:ruff/format 干净、mypy **90 文件**、pytest **169 passed**(114→+37+18)。memory 登记 C6 扩 + 2 decisions + 2 gotchas。下一步 **Wave B:T3.2(@backend 到期扫描+登记接口) ‖ T3.3(@llm foreshadow/pace 第三审并入 review_specs)**。 +- [2026-06-18] @orchestrator — **进入 M3**:归档 Phase 2(M2) 至已归档;Phase 3 七任务拉入台账。沿用 M2 节奏:契约先行 + 分目录并行 + 全仓门禁收口。核对:`foreshadow` 表已在 T0.2 建齐 → **M3 无新建表迁移**;T3.1「Repository+状态机」据实记 @backend(落 `core/ww_core/domain/`,同 T2.3 先例);T3.3 复用 M2 `build_review_graph` 的 `review_specs` 扩展点加 foreshadow/pace 两审(文风第四审留 M4)。启动 **Wave A:T3.1(@backend·core/domain) ‖ T3.4(@llm·packages/agents+orchestrator)**(不同目录,无写冲突)。 - [2026-06-18] @orchestrator — **🎉 M2 完成**(Wave E:T2.7✅)。`tests/test_m2_e2e.py` 真实 pg + 多档位 mock 网关(零 token)真跑通过 2 用例:主用例串 立项→写章 SSE→自动保存→**审稿 SSE(section/conflict)**→GET 历史→**残缺裁决触发 409 `CONFLICT_UNRESOLVED`+missing_indices(gate 事务前拦截未写库)**→全量裁决+终稿验收(200, accepted v2)→**DB 真源逐项断言**:chapters draft v1 留存 + accepted v2(content==final_text)、`chapter_digests.facts` 追溯终稿标记(证 #4 digest 从终稿非草稿)、`chapter_reviews.decisions` 写回、`usage_ledger` 三档位(writer/analyst/light)各落一条且事务已 commit;补充用例验证零冲突直通。**无新 bug**(M1 曾暴露的 ledger 提交 bug 在 M2 三档位无复发——review 流末 + accept 事务末均 commit)。最终全收口门禁绿:ruff/format 干净、mypy **90 文件**、pytest **114 passed**、**alembic 无漂移**(M2 全程零建表迁移)。M2 出口 DoD 全达成。下一阶段 **M3(伏笔+节奏)**。 - [2026-06-18] @orchestrator — **Wave D 完成**(T2.6✅)。审稿报告页落地:`app/projects/[id]/review`(RSC 取历史)+ `components/review/*`(报告卡/就地标注/AcceptPanel)+ `lib/review/*`(SSE reducer/裁决完整性/请求体组装,纯逻辑单测)。`pnpm gen:api` 纳入 review/accept/reviews 三端点类型(不手写);SSE 沿用 `useDraftStream` 的 fetch+ReadableStream 范式;五类冲突就地朱砂锚点(R6 段级降级)+裁决(采纳/忽略/手改)+**未决禁验收 gate**+409 missing_indices 高亮+「本次将更新」清单。前端门禁绿:gen:api/lint/tsc/**vitest 45(+21)**/build。memory +1 decision +2 gotchas。下一步 **Wave E:T2.7 M2 E2E**(@qa 真实 DB + mock 网关零 token)。 - [2026-06-18] @orchestrator — **Wave C 完成**(T2.4✅ + T2.5✅,C3 扩稳定,单 @backend agent 顺序做)。三端点入 OpenAPI:`POST .../review`(SSE section/conflict/done) + `GET .../reviews`(新→旧) + `POST .../accept`。验收:冲突 gate(裁决 `conflict_index` 须覆盖 review.conflicts 全下标,缺判→409 `CONFLICT_UNRESOLVED`+missing_indices)→ 终稿 digest 提炼在事务外(R2,light 档)→ 单原子事务 promote(R4)+digest.append(#4)+set_decisions、末尾一次 commit、失败全回滚(R3)。`build_gateway_for_tier` 统一多档位网关注入。全仓门禁绿:ruff/format 干净、mypy **83 文件**、pytest **112 passed**(+9)。memory 登记 C3 扩 + 1 decision + 2 gotchas。下一步 **Wave D:T2.6**(@frontend 审稿页+裁决+验收 gate,先 `pnpm gen:api`)。 diff --git a/apps/api/tests/fakes_projects.py b/apps/api/tests/fakes_projects.py index 80b2662..17e5294 100644 --- a/apps/api/tests/fakes_projects.py +++ b/apps/api/tests/fakes_projects.py @@ -8,10 +8,19 @@ from __future__ import annotations import uuid from collections.abc import AsyncIterator +from contextlib import asynccontextmanager from typing import Any from pydantic import BaseModel +from ww_core.domain import ForeshadowLedgerView, OutlineWriteView from ww_core.domain.chapter_repo import ChapterDraftView, ChapterView +from ww_core.domain.foreshadow_state import ( + ForeshadowStatus, + apply_overdue_scan, +) +from ww_core.domain.foreshadow_state import ( + transition as apply_transition, +) from ww_core.domain.project_repo import ProjectCreate, ProjectView from ww_core.domain.repositories import DigestView from ww_core.domain.review_repo import ReviewView @@ -172,14 +181,156 @@ class FakeDigestAppendRepo: class FakeSession: - """最小 fake:记录 commit 次数(验收事务边界断言)。""" + """最小 fake:记录 commit/rollback 次数(验收事务 + 端点提交边界断言)。""" def __init__(self) -> None: self.commits = 0 + self.rollbacks = 0 async def commit(self) -> None: self.commits += 1 + async def rollback(self) -> None: + self.rollbacks += 1 + + +class FakeForeshadowRepo: + """实现 `ForeshadowLedgerRepo` Protocol(内存;状态机/进展/扫描复用纯函数)。 + + `raise_integrity_on` 模拟重复 code 的 DB 唯一约束冲突(register 抛 IntegrityError)。 + """ + + def __init__(self, *, raise_integrity_on: set[str] | None = None) -> None: + self.rows: dict[tuple[uuid.UUID, str], ForeshadowLedgerView] = {} + self._raise_integrity_on = raise_integrity_on or set() + + async def register( + self, + project_id: uuid.UUID, + *, + code: str, + title: str, + planted_at: int | None = None, + content: str | None = None, + expected_close_from: int | None = None, + expected_close_to: int | None = None, + importance: str | None = None, + ) -> ForeshadowLedgerView: + if code in self._raise_integrity_on or (project_id, code) in self.rows: + from sqlalchemy.exc import IntegrityError + + raise IntegrityError("duplicate code", None, Exception("unique")) + view = ForeshadowLedgerView( + code=code, + title=title, + status=ForeshadowStatus.OPEN.value, + planted_at=planted_at, + content=content, + expected_close_from=expected_close_from, + expected_close_to=expected_close_to, + importance=importance, + ) + self.rows[(project_id, code)] = view + return view + + async def get(self, project_id: uuid.UUID, code: str) -> ForeshadowLedgerView | None: + return self.rows.get((project_id, code)) + + async def list_by_status( + self, project_id: uuid.UUID, status: str | None = None + ) -> list[ForeshadowLedgerView]: + rows = [v for (p, _), v in self.rows.items() if p == project_id] + if status is not None: + rows = [v for v in rows if v.status == status] + return sorted(rows, key=lambda v: v.code) + + async def transition( + self, project_id: uuid.UUID, code: str, *, to_status: str + ) -> ForeshadowLedgerView: + row = self.rows.get((project_id, code)) + if row is None: + raise LookupError(f"foreshadow not found: {code}") + new_status = apply_transition( + ForeshadowStatus(row.status), ForeshadowStatus(to_status) + ).value + updated = row.model_copy(update={"status": new_status}) + self.rows[(project_id, code)] = updated + return updated + + async def record_progress( + self, project_id: uuid.UUID, code: str, *, entry: dict[str, Any] + ) -> ForeshadowLedgerView: + row = self.rows.get((project_id, code)) + if row is None: + raise LookupError(f"foreshadow not found: {code}") + updated = row.model_copy(update={"progress": [*row.progress, dict(entry)]}) + self.rows[(project_id, code)] = updated + return updated + + async def scan_overdue( + self, project_id: uuid.UUID, *, current_chapter: int + ) -> list[ForeshadowLedgerView]: + changed: list[ForeshadowLedgerView] = [] + for key, row in list(self.rows.items()): + new_status = apply_overdue_scan( + ForeshadowStatus(row.status), + current_chapter=current_chapter, + expected_close_to=row.expected_close_to, + ).value + if new_status != row.status: + updated = row.model_copy(update={"status": new_status}) + self.rows[key] = updated + changed.append(updated) + return changed + + +class FakeOutlineWriteRepo: + """实现 `OutlineWriteRepo` Protocol(内存;幂等覆盖同 (project_id, chapter_no))。""" + + def __init__(self) -> None: + # (project_id, chapter_no) -> (volume, beats, foreshadow_windows) + self.rows: dict[tuple[uuid.UUID, int], tuple[int, list[str], list[dict[str, Any]]]] = {} + self.upsert_calls = 0 + + async def upsert_chapter( + self, + project_id: uuid.UUID, + *, + volume: int, + chapter_no: int, + beats: list[str], + foreshadow_windows: list[dict[str, Any]], + ) -> OutlineWriteView: + self.upsert_calls += 1 + windows = [dict(w) for w in foreshadow_windows] + self.rows[(project_id, chapter_no)] = (volume, list(beats), windows) + return OutlineWriteView( + chapter_no=chapter_no, + volume=volume, + beats=list(beats), + foreshadow_windows=windows, + ) + + +class FakeSessionFactory: + """到期扫描的独立 session 工厂替身:`()` → async-CM 产 `FakeSession`。 + + 记录开了几次 session + 各次 commit,供「扫描自建 session 并提交」断言。 + """ + + def __init__(self) -> None: + self.sessions: list[FakeSession] = [] + + def __call__(self) -> Any: + session = FakeSession() + self.sessions.append(session) + + @asynccontextmanager + async def _cm() -> AsyncIterator[FakeSession]: + yield session + + return _cm() + class FakeReviewGateway: """实现审稿/digest 节点最小依赖(`run`):吐固定结构化 `parsed`,绝不联网。""" diff --git a/apps/api/tests/test_foreshadow.py b/apps/api/tests/test_foreshadow.py new file mode 100644 index 0000000..450fcde --- /dev/null +++ b/apps/api/tests/test_foreshadow.py @@ -0,0 +1,203 @@ +"""T3.2 伏笔登记/状态变更端点 + 验收后到期扫描(内存替身,无 DB/无网络)。 + +覆盖: +- 登记 201 + 重复 code → 422 VALIDATION 信封; +- 状态机转移 200 / 非法转移 → 422 VALIDATION / 不存在 → 404; +- 进展 append;空更新 → 422; +- 到期扫描纯函数 `run_overdue_scan` 直接 await:埋 expected_close_to < 验收章号的伏笔 + → 置 OVERDUE、自建 session 提交(**绝不真起后台线程**)。 +""" + +from __future__ import annotations + +import uuid + +import httpx +import pytest +from fakes_projects import ( + FakeForeshadowRepo, + FakeSession, + FakeSessionFactory, +) +from ww_api.services.foreshadow_scan import run_overdue_scan +from ww_core.domain.foreshadow_state import ForeshadowStatus +from ww_shared import ErrorCode + + +def _make_client( + *, repo: FakeForeshadowRepo | None = None, session: FakeSession | None = None +) -> tuple[httpx.AsyncClient, FakeForeshadowRepo, FakeSession]: + import os + + os.environ.setdefault("CREDENTIAL_ENC_KEY", "x" * 44) + from ww_api.main import create_app + from ww_api.services.project_deps import get_foreshadow_repo + from ww_db import get_session + + repo = repo or FakeForeshadowRepo() + session = session or FakeSession() + + app = create_app() + app.dependency_overrides[get_foreshadow_repo] = lambda: repo + app.dependency_overrides[get_session] = lambda: session + transport = httpx.ASGITransport(app=app) + client = httpx.AsyncClient(transport=transport, base_url="http://test") + return client, repo, session + + +# ---- 登记 ---- + + +@pytest.mark.asyncio +async def test_register_foreshadow_returns_201_open() -> None: + client, repo, session = _make_client() + pid = uuid.uuid4() + async with client: + resp = await client.post( + f"/projects/{pid}/foreshadow", + json={"code": "F1", "title": "神秘信物", "planted_at": 3, "expected_close_to": 10}, + ) + assert resp.status_code == 201 + body = resp.json() + assert body["code"] == "F1" + assert body["status"] == ForeshadowStatus.OPEN.value + assert body["expected_close_to"] == 10 + assert session.commits == 1 + assert (pid, "F1") in repo.rows + + +@pytest.mark.asyncio +async def test_register_duplicate_code_maps_to_422_validation() -> None: + repo = FakeForeshadowRepo(raise_integrity_on={"F1"}) + client, _, session = _make_client(repo=repo) + pid = uuid.uuid4() + async with client: + resp = await client.post( + f"/projects/{pid}/foreshadow", json={"code": "F1", "title": "重复"} + ) + assert resp.status_code == 422 + err = resp.json()["error"] + assert err["code"] == ErrorCode.VALIDATION + assert err["details"]["reason"] == "duplicate" + # 冲突后回滚、不提交。 + assert session.commits == 0 + assert session.rollbacks == 1 + + +# ---- 状态变更 ---- + + +@pytest.mark.asyncio +async def test_transition_open_to_partial_200() -> None: + repo = FakeForeshadowRepo() + pid = uuid.uuid4() + await repo.register(pid, code="F1", title="t") + client, _, session = _make_client(repo=repo) + async with client: + resp = await client.patch(f"/projects/{pid}/foreshadow/F1", json={"to_status": "PARTIAL"}) + assert resp.status_code == 200 + assert resp.json()["status"] == "PARTIAL" + assert session.commits == 1 + + +@pytest.mark.asyncio +async def test_invalid_transition_maps_to_422_validation() -> None: + repo = FakeForeshadowRepo() + pid = uuid.uuid4() + await repo.register(pid, code="F1", title="t") + await repo.transition(pid, "F1", to_status="CLOSED") # 先到终态 + client, _, session = _make_client(repo=repo) + async with client: + resp = await client.patch( + f"/projects/{pid}/foreshadow/F1", + json={"to_status": "PARTIAL"}, # CLOSED→PARTIAL 非法 + ) + assert resp.status_code == 422 + err = resp.json()["error"] + assert err["code"] == ErrorCode.VALIDATION + assert err["details"]["reason"] == "invalid_transition" + assert session.rollbacks == 1 + + +@pytest.mark.asyncio +async def test_transition_unknown_code_404() -> None: + client, _, _ = _make_client() + pid = uuid.uuid4() + async with client: + resp = await client.patch(f"/projects/{pid}/foreshadow/NOPE", json={"to_status": "PARTIAL"}) + assert resp.status_code == 404 + assert resp.json()["error"]["code"] == ErrorCode.NOT_FOUND + + +@pytest.mark.asyncio +async def test_record_progress_appends() -> None: + repo = FakeForeshadowRepo() + pid = uuid.uuid4() + await repo.register(pid, code="F1", title="t") + client, _, session = _make_client(repo=repo) + async with client: + resp = await client.patch( + f"/projects/{pid}/foreshadow/F1", + json={"progress_entry": {"chapter_no": 5, "note": "首次照应"}}, + ) + assert resp.status_code == 200 + assert resp.json()["progress"][0]["note"] == "首次照应" + assert session.commits == 1 + + +@pytest.mark.asyncio +async def test_empty_update_maps_to_422() -> None: + repo = FakeForeshadowRepo() + pid = uuid.uuid4() + await repo.register(pid, code="F1", title="t") + client, _, _ = _make_client(repo=repo) + async with client: + resp = await client.patch(f"/projects/{pid}/foreshadow/F1", json={}) + assert resp.status_code == 422 + assert resp.json()["error"]["details"]["reason"] == "empty_update" + + +# ---- 验收后到期扫描(纯函数直接 await,不起后台线程)---- + + +@pytest.mark.asyncio +async def test_run_overdue_scan_marks_overdue_and_commits() -> None: + # Arrange:一条 expected_close_to=5 的伏笔,验收章号 7 > 5 → 应置 OVERDUE。 + repo = FakeForeshadowRepo() + pid = uuid.uuid4() + await repo.register(pid, code="F1", title="逾期", expected_close_to=5) + await repo.register(pid, code="F2", title="窗口内", expected_close_to=9) + factory = FakeSessionFactory() + + # Act:经 repo_factory 缝注入 fake 账本 repo(不联网/不真起后台线程)。 + count = await run_overdue_scan( + factory, + project_id=pid, + chapter_no=7, + request_id="req-1", + repo_factory=lambda _session: repo, + ) + + # Assert + assert count == 1 + assert repo.rows[(pid, "F1")].status == ForeshadowStatus.OVERDUE.value + assert repo.rows[(pid, "F2")].status == ForeshadowStatus.OPEN.value + # 自建 session 且有变更 → 提交一次。 + assert len(factory.sessions) == 1 + assert factory.sessions[0].commits == 1 + + +@pytest.mark.asyncio +async def test_run_overdue_scan_no_change_skips_commit() -> None: + repo = FakeForeshadowRepo() + pid = uuid.uuid4() + await repo.register(pid, code="F1", title="窗口内", expected_close_to=9) + factory = FakeSessionFactory() + + count = await run_overdue_scan( + factory, project_id=pid, chapter_no=3, repo_factory=lambda _session: repo + ) + + assert count == 0 + # 空扫描不提交。 + assert factory.sessions[0].commits == 0 diff --git a/apps/api/tests/test_outline.py b/apps/api/tests/test_outline.py new file mode 100644 index 0000000..ba23ad2 --- /dev/null +++ b/apps/api/tests/test_outline.py @@ -0,0 +1,216 @@ +"""T3.5 端点测试:大纲生成 + 持久化 + 伏笔看板(内存替身,无 DB/无网络)。 + +覆盖: +- POST /outline:mock 网关产 `OutlineResult` → 逐章 upsert(含 foreshadow_windows)→ 端点 commit; +- 幂等:同 (project_id, chapter_no) 再生成覆盖; +- 项目不存在 → 404;无凭据 → LLM_UNAVAILABLE(503); +- GET /foreshadow?status=:按状态过滤;非法 status → 422 VALIDATION。 +""" + +from __future__ import annotations + +import os +import uuid + +import httpx +import pytest +from fakes_projects import ( + FakeForeshadowRepo, + FakeOutlineWriteRepo, + FakeProjectRepo, + FakeReviewGateway, + FakeSession, +) +from test_projects import _empty_memory_repos +from ww_agents import ForeshadowWindow, OutlineChapter, OutlineResult +from ww_core.domain.project_repo import ProjectCreate +from ww_shared import AppError, ErrorCode + + +def _outline_result() -> OutlineResult: + return OutlineResult( + chapters=[ + OutlineChapter( + no=1, + beats=["开篇引入主角", "埋下信物伏笔"], + foreshadow_windows=[ + ForeshadowWindow(code="F1", plant_chapter=1, expected_close_to=10) + ], + ), + OutlineChapter(no=2, beats=["冲突升级"], foreshadow_windows=[]), + ] + ) + + +def _make_client( + *, + project_repo: FakeProjectRepo | None = None, + foreshadow_repo: FakeForeshadowRepo | None = None, + outline_repo: FakeOutlineWriteRepo | None = None, + gateway: FakeReviewGateway | None = None, + session: FakeSession | None = None, +) -> tuple[httpx.AsyncClient, FakeOutlineWriteRepo, FakeSession]: + os.environ.setdefault("CREDENTIAL_ENC_KEY", "x" * 44) + from ww_api.main import create_app + from ww_api.services.project_deps import ( + get_foreshadow_repo, + get_memory_repos, + get_outline_gateway, + get_outline_write_repo, + get_project_repo, + ) + from ww_db import get_session + + project_repo = project_repo or FakeProjectRepo() + foreshadow_repo = foreshadow_repo or FakeForeshadowRepo() + outline_repo = outline_repo or FakeOutlineWriteRepo() + gateway = gateway or FakeReviewGateway(parsed=_outline_result()) + session = session or FakeSession() + + app = create_app() + app.dependency_overrides[get_project_repo] = lambda: project_repo + app.dependency_overrides[get_foreshadow_repo] = lambda: foreshadow_repo + app.dependency_overrides[get_memory_repos] = _empty_memory_repos + app.dependency_overrides[get_outline_write_repo] = lambda: outline_repo + app.dependency_overrides[get_session] = lambda: session + app.dependency_overrides[get_outline_gateway] = lambda: gateway + + transport = httpx.ASGITransport(app=app, raise_app_exceptions=False) + client = httpx.AsyncClient(transport=transport, base_url="http://test") + return client, outline_repo, session + + +async def _seed_project(repo: FakeProjectRepo) -> uuid.UUID: + view = await repo.create(uuid.UUID(int=1), ProjectCreate(title="测试作品", genre="玄幻")) + return uuid.UUID(str(view.id)) + + +# ---- 大纲生成 ---- + + +@pytest.mark.asyncio +async def test_generate_outline_upserts_chapters_and_commits() -> None: + project_repo = FakeProjectRepo() + pid = await _seed_project(project_repo) + client, outline_repo, session = _make_client(project_repo=project_repo) + + async with client: + resp = await client.post(f"/projects/{pid}/outline", json={"volume": 1}) + + assert resp.status_code == 200 + body = resp.json() + assert [c["no"] for c in body["chapters"]] == [1, 2] + ch1 = body["chapters"][0] + assert ch1["beats"] == ["开篇引入主角", "埋下信物伏笔"] + assert ch1["foreshadow_windows"][0]["code"] == "F1" + assert ch1["foreshadow_windows"][0]["expected_close_to"] == 10 + # 逐章 upsert(2 章)+ 端点末尾一次 commit。 + assert outline_repo.upsert_calls == 2 + assert (pid, 1) in outline_repo.rows + assert outline_repo.rows[(pid, 1)][1] == ["开篇引入主角", "埋下信物伏笔"] + assert session.commits == 1 + + +@pytest.mark.asyncio +async def test_generate_outline_is_idempotent_overwrites_same_chapter() -> None: + project_repo = FakeProjectRepo() + pid = await _seed_project(project_repo) + outline_repo = FakeOutlineWriteRepo() + client, _, _ = _make_client(project_repo=project_repo, outline_repo=outline_repo) + + async with client: + await client.post(f"/projects/{pid}/outline", json={"volume": 1}) + await client.post(f"/projects/{pid}/outline", json={"volume": 1}) + + # 同 (project_id, chapter_no) 覆盖,不堆叠 → 仍只 2 行。 + assert len(outline_repo.rows) == 2 + + +@pytest.mark.asyncio +async def test_generate_outline_unknown_project_404() -> None: + client, _, _ = _make_client() + async with client: + resp = await client.post(f"/projects/{uuid.uuid4()}/outline", json={}) + assert resp.status_code == 404 + assert resp.json()["error"]["code"] == ErrorCode.NOT_FOUND + + +@pytest.mark.asyncio +async def test_generate_outline_without_credentials_maps_to_llm_unavailable() -> None: + # 无凭据时 `build_gateway_for_tier` 抛 LLM_UNAVAILABLE(503),在网关 dep 解析阶段发生。 + project_repo = FakeProjectRepo() + pid = await _seed_project(project_repo) + session = FakeSession() + os.environ.setdefault("CREDENTIAL_ENC_KEY", "x" * 44) + from ww_api.main import create_app + from ww_api.services.project_deps import ( + get_foreshadow_repo, + get_memory_repos, + get_outline_gateway, + get_outline_write_repo, + get_project_repo, + ) + from ww_db import get_session + + async def _no_creds() -> object: + raise AppError(ErrorCode.LLM_UNAVAILABLE, "未配置凭据", {"provider": "deepseek"}) + + app = create_app() + app.dependency_overrides[get_project_repo] = lambda: project_repo + app.dependency_overrides[get_foreshadow_repo] = lambda: FakeForeshadowRepo() + app.dependency_overrides[get_memory_repos] = _empty_memory_repos + app.dependency_overrides[get_outline_write_repo] = lambda: FakeOutlineWriteRepo() + app.dependency_overrides[get_session] = lambda: session + app.dependency_overrides[get_outline_gateway] = _no_creds + transport = httpx.ASGITransport(app=app, raise_app_exceptions=False) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: + resp = await client.post(f"/projects/{pid}/outline", json={}) + + assert resp.status_code == 503 + assert resp.json()["error"]["code"] == ErrorCode.LLM_UNAVAILABLE + # 网关阶段失败 → 未生成、未提交。 + assert session.commits == 0 + + +@pytest.mark.asyncio +async def test_generate_outline_upper_error_propagates() -> None: + project_repo = FakeProjectRepo() + pid = await _seed_project(project_repo) + gateway = FakeReviewGateway(error=AppError(ErrorCode.LLM_UNAVAILABLE, "boom")) + client, _, session = _make_client(project_repo=project_repo, gateway=gateway) + + async with client: + resp = await client.post(f"/projects/{pid}/outline", json={}) + + assert resp.status_code == 503 + assert session.commits == 0 + + +# ---- 伏笔看板 ---- + + +@pytest.mark.asyncio +async def test_foreshadow_board_filters_by_status() -> None: + repo = FakeForeshadowRepo() + pid = uuid.uuid4() + await repo.register(pid, code="F1", title="开放伏笔") + await repo.register(pid, code="F2", title="部分回收") + await repo.transition(pid, "F2", to_status="PARTIAL") + from ww_api.main import create_app + from ww_api.services.project_deps import get_foreshadow_repo + + os.environ.setdefault("CREDENTIAL_ENC_KEY", "x" * 44) + app = create_app() + app.dependency_overrides[get_foreshadow_repo] = lambda: repo + transport = httpx.ASGITransport(app=app, raise_app_exceptions=False) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: + all_resp = await client.get(f"/projects/{pid}/foreshadow") + open_resp = await client.get(f"/projects/{pid}/foreshadow", params={"status": "OPEN"}) + bad_resp = await client.get(f"/projects/{pid}/foreshadow", params={"status": "NOPE"}) + + assert {f["code"] for f in all_resp.json()["foreshadow"]} == {"F1", "F2"} + open_codes = [f["code"] for f in open_resp.json()["foreshadow"]] + assert open_codes == ["F1"] + assert bad_resp.status_code == 422 + assert bad_resp.json()["error"]["code"] == ErrorCode.VALIDATION + assert bad_resp.json()["error"]["details"]["reason"] == "invalid_status" diff --git a/apps/api/tests/test_outline_context.py b/apps/api/tests/test_outline_context.py new file mode 100644 index 0000000..f9b9385 --- /dev/null +++ b/apps/api/tests/test_outline_context.py @@ -0,0 +1,63 @@ +"""T3.5 大纲注入上下文组装单测(确定性纯函数;ARCH §5.4)。 + +断言:四源(设定/伏笔/人物/世界观)都进文本、排序确定(同输入同输出)、空源不炸。 +""" + +from __future__ import annotations + +from ww_api.services.outline_context import build_outline_context +from ww_core.domain import ForeshadowLedgerView +from ww_core.domain.project_repo import ProjectView +from ww_core.domain.repositories import CharacterView, WorldEntityView + + +def _project() -> ProjectView: + import uuid + + return ProjectView( + id=uuid.UUID(int=1), + title="逆天剑帝", + genre="玄幻", + logline="少年逆袭", + selling_points=["爽点密集", "升级流"], + ) + + +def test_build_context_includes_all_four_sources() -> None: + text = build_outline_context( + project=_project(), + foreshadow=[ + ForeshadowLedgerView( + code="F1", title="神秘信物", status="OPEN", planted_at=3, expected_close_to=10 + ) + ], + characters=[CharacterView(name="主角", role="protagonist", motive="复仇")], + world_entities=[WorldEntityView(type="location", name="剑冢")], + ) + assert "逆天剑帝" in text + assert "F1" in text and "神秘信物" in text + assert "回收窗口 ?-10" in text + assert "主角" in text + assert "剑冢" in text + + +def test_build_context_is_deterministic_and_sorted() -> None: + project = _project() + fs = [ + ForeshadowLedgerView(code="F2", title="b", status="OPEN"), + ForeshadowLedgerView(code="F1", title="a", status="OPEN"), + ] + a = build_outline_context(project=project, foreshadow=fs, characters=[], world_entities=[]) + b = build_outline_context( + project=project, foreshadow=list(reversed(fs)), characters=[], world_entities=[] + ) + assert a == b + # F1 排在 F2 前(按 code 升序)。 + assert a.index("[F1]") < a.index("[F2]") + + +def test_build_context_handles_empty_sources() -> None: + text = build_outline_context( + project=_project(), foreshadow=[], characters=[], world_entities=[] + ) + assert "(无)" in text diff --git a/apps/api/tests/test_review_accept.py b/apps/api/tests/test_review_accept.py index fc8dc5c..de7dc07 100644 --- a/apps/api/tests/test_review_accept.py +++ b/apps/api/tests/test_review_accept.py @@ -17,6 +17,7 @@ from fakes_projects import ( FakeReviewGateway, FakeReviewRepo, FakeSession, + FakeSessionFactory, ) from test_projects import _empty_memory_repos from ww_agents import ContinuityReview @@ -50,6 +51,7 @@ def _make_client( get_memory_repos, get_review_gateway, get_review_repo, + get_session_factory, ) from ww_db import get_session @@ -68,6 +70,7 @@ def _make_client( app.dependency_overrides[get_review_gateway] = lambda: review_gateway app.dependency_overrides[get_digest_gateway] = lambda: digest_gateway app.dependency_overrides[get_session] = lambda: session + app.dependency_overrides[get_session_factory] = lambda: FakeSessionFactory() transport = httpx.ASGITransport(app=app) client = httpx.AsyncClient(transport=transport, base_url="http://test") return client, chapter_repo, review_repo, digest_repo, session diff --git a/apps/api/ww_api/main.py b/apps/api/ww_api/main.py index 9d4f3d4..deeb41b 100644 --- a/apps/api/ww_api/main.py +++ b/apps/api/ww_api/main.py @@ -12,7 +12,14 @@ from ww_shared import AppError, ErrorBody, ErrorEnvelope from ww_api.logging_config import configure_logging, get_logger from ww_api.middleware import request_id_middleware -from ww_api.routers import health, jobs, projects, settings_providers +from ww_api.routers import ( + foreshadow, + health, + jobs, + outline, + projects, + settings_providers, +) from ww_api.services.project_deps import seed_stub_user configure_logging() @@ -48,6 +55,8 @@ def create_app() -> FastAPI: app.include_router(health.router) app.include_router(jobs.router) app.include_router(projects.router) + app.include_router(foreshadow.router) + app.include_router(outline.router) app.include_router(settings_providers.router) return app diff --git a/apps/api/ww_api/routers/foreshadow.py b/apps/api/ww_api/routers/foreshadow.py new file mode 100644 index 0000000..c119ae5 --- /dev/null +++ b/apps/api/ww_api/routers/foreshadow.py @@ -0,0 +1,159 @@ +"""伏笔登记 / 状态变更端点(C3 扩 / ARCH §6.2, §7.2;不变量 #3)。 + +- POST /projects/:id/foreshadow 作者显式登记一条伏笔(status=OPEN)。 +- PATCH /projects/:id/foreshadow/:code 状态机转移 和/或 追加进展。 + +登记/改状态是**作者显式动作**(不变量 #3:伏笔入库不经 AI 静默写库;验收后到期扫描 +是确定性纯函数置位,见 `services/foreshadow_scan.py`)。 + +提交边界:`ForeshadowLedgerRepo` 写方法只 `flush()`,端点写后 `await session.commit()`。 + +**T3.5 续接点**:本 router 是 foreshadow 端点的归属处。T3.5 的看板查询 +`GET /projects/:id/foreshadow?status=` 直接加到本 router(调 `repo.list_by_status`); +大纲 `POST /projects/:id/outline` 与伏笔无关,建议落独立 `routers/outline.py` +(或 projects router),不混进本文件。 +""" + +from __future__ import annotations + +import uuid +from typing import Annotated + +from fastapi import APIRouter, Depends, Request +from sqlalchemy.exc import IntegrityError +from sqlalchemy.ext.asyncio import AsyncSession +from ww_core.domain import ForeshadowLedgerRepo, ForeshadowLedgerView +from ww_core.domain.foreshadow_state import ForeshadowStatus, InvalidTransition +from ww_db import get_session +from ww_shared import AppError, ErrorCode + +from ww_api.logging_config import get_logger +from ww_api.schemas.foreshadow import ( + ForeshadowBoardResponse, + ForeshadowRegisterRequest, + ForeshadowTransitionRequest, + ForeshadowView, +) +from ww_api.services.project_deps import get_foreshadow_repo + +log = get_logger("ww.api.foreshadow") + +router = APIRouter(prefix="/projects", tags=["foreshadow"]) + +ForeshadowRepoDep = Annotated[ForeshadowLedgerRepo, Depends(get_foreshadow_repo)] +SessionDep = Annotated[AsyncSession, Depends(get_session)] + + +def _to_view(v: ForeshadowLedgerView) -> ForeshadowView: + # ForeshadowLedgerView 与 ForeshadowView 字段同名,逐字段映射(snake_case 契约)。 + return ForeshadowView.model_validate(v, from_attributes=True) + + +@router.post("/{project_id}/foreshadow", status_code=201) +async def register_foreshadow( + project_id: uuid.UUID, + body: ForeshadowRegisterRequest, + request: Request, + repo: ForeshadowRepoDep, + session: SessionDep, +) -> ForeshadowView: + """登记一条伏笔(status=OPEN)。重复 `code` → DB 唯一约束冲突 → VALIDATION 信封。""" + request_id = getattr(request.state, "request_id", None) + try: + view = await repo.register( + project_id, + code=body.code, + title=body.title, + planted_at=body.planted_at, + content=body.content, + expected_close_from=body.expected_close_from, + expected_close_to=body.expected_close_to, + importance=body.importance, + ) + await session.commit() + except IntegrityError as exc: + # `(project_id, code)` 唯一约束冲突(register 仅 INSERT、不 upsert,见 foreshadow_repo)。 + await session.rollback() + raise AppError( + ErrorCode.VALIDATION, + f"伏笔代号已存在:{body.code}", + {"field": "code", "code": body.code, "reason": "duplicate"}, + ) from exc + log.info( + "foreshadow_registered", + project_id=str(project_id), + request_id=request_id, + code=body.code, + ) + return _to_view(view) + + +@router.get("/{project_id}/foreshadow") +async def list_foreshadow( + project_id: uuid.UUID, + repo: ForeshadowRepoDep, + status: str | None = None, +) -> ForeshadowBoardResponse: + """伏笔看板:按 `status` 过滤(缺省=全部),按 code 升序。 + + `status` 非法(不在 OPEN/PARTIAL/CLOSED/OVERDUE)→ VALIDATION 信封。四泳道前端 + 据 `status` 分组;OVERDUE 泳道 + 逾期标记用 `expected_close_to`(看板字段已齐)。 + """ + if status is not None and status not in {s.value for s in ForeshadowStatus}: + raise AppError( + ErrorCode.VALIDATION, + f"非法状态过滤:{status}", + {"field": "status", "reason": "invalid_status"}, + ) + views = await repo.list_by_status(project_id, status) + return ForeshadowBoardResponse(foreshadow=[_to_view(v) for v in views]) + + +@router.patch("/{project_id}/foreshadow/{code}") +async def update_foreshadow( + project_id: uuid.UUID, + code: str, + body: ForeshadowTransitionRequest, + request: Request, + repo: ForeshadowRepoDep, + session: SessionDep, +) -> ForeshadowView: + """状态机转移 和/或 追加进展。非法转移 → VALIDATION 信封;不存在 → NOT_FOUND。 + + `to_status` 与 `progress_entry` 皆可选;两者都缺 → VALIDATION(无操作)。先转移、后追加。 + """ + request_id = getattr(request.state, "request_id", None) + if body.to_status is None and body.progress_entry is None: + raise AppError( + ErrorCode.VALIDATION, + "至少提供 `to_status` 或 `progress_entry` 之一", + {"reason": "empty_update"}, + ) + + view: ForeshadowLedgerView + try: + if body.to_status is not None: + view = await repo.transition(project_id, code, to_status=body.to_status) + if body.progress_entry is not None: + view = await repo.record_progress(project_id, code, entry=body.progress_entry) + await session.commit() + except InvalidTransition as exc: + await session.rollback() + raise AppError( + ErrorCode.VALIDATION, + str(exc), + {"field": "to_status", "code": code, "reason": "invalid_transition"}, + ) from exc + except LookupError as exc: + await session.rollback() + raise AppError(ErrorCode.NOT_FOUND, f"foreshadow not found: {code}") from exc + + log.info( + "foreshadow_updated", + project_id=str(project_id), + request_id=request_id, + code=code, + to_status=body.to_status, + has_progress=body.progress_entry is not None, + ) + return _to_view(view) diff --git a/apps/api/ww_api/routers/outline.py b/apps/api/ww_api/routers/outline.py new file mode 100644 index 0000000..cde080a --- /dev/null +++ b/apps/api/ww_api/routers/outline.py @@ -0,0 +1,135 @@ +"""大纲生成端点(C3 扩 / ARCH §5.4 outliner / §7.2 POST /outline;不变量 #2/#3)。 + +POST /projects/:id/outline:作者显式发起大纲生成 + 持久化。 + +流程:组确定性上下文(设定 + 已登记伏笔 + 人物 + 世界观)→ `run_outline`(analyst 网关, +结构化 `OutlineResult`)→ 逐章 upsert `outline` 表 → 端点末尾一次 `commit()`。 + +提交边界:网关 ledger 只 flush(run_outline 产 1 条 usage)、`OutlineWriteRepo.upsert_chapter` +只 flush → 端点末尾 `await session.commit()`(否则 usage_ledger + outline 行静默丢失,见 gotcha)。 + +不变量 #2:经 `build_gateway_for_tier(..., "analyst")` 注入;outliner_spec 只声明档位不传 model。 +不变量 #3:outliner `writes=["outline"]` 经此**作者发起**端点落地,非 AI 静默写其它表; +run_outline 节点本身只读不写库。无凭据 → `LLM_UNAVAILABLE`(友好提示,仿 draft/review)。 +""" + +from __future__ import annotations + +import uuid +from typing import Annotated + +from fastapi import APIRouter, Depends, Request +from sqlalchemy.ext.asyncio import AsyncSession +from ww_agents import outliner_spec +from ww_core.domain import ForeshadowLedgerRepo, OutlineWriteRepo, ProjectRepo +from ww_core.domain.repositories import MemoryRepos +from ww_core.orchestrator import run_outline +from ww_db import get_session +from ww_llm_gateway import Gateway +from ww_shared import AppError, ErrorCode + +from ww_api.logging_config import get_logger +from ww_api.schemas.outline import ( + ForeshadowWindowView, + OutlineChapterView, + OutlineGenerateRequest, + OutlineResponse, +) +from ww_api.services.credentials import STUB_OWNER_ID +from ww_api.services.outline_context import build_outline_context +from ww_api.services.project_deps import ( + get_foreshadow_repo, + get_memory_repos, + get_outline_gateway, + get_outline_write_repo, + get_project_repo, +) + +log = get_logger("ww.api.outline") + +router = APIRouter(prefix="/projects", tags=["outline"]) + +ProjectRepoDep = Annotated[ProjectRepo, Depends(get_project_repo)] +ForeshadowRepoDep = Annotated[ForeshadowLedgerRepo, Depends(get_foreshadow_repo)] +MemoryReposDep = Annotated[MemoryRepos, Depends(get_memory_repos)] +OutlineWriteRepoDep = Annotated[OutlineWriteRepo, Depends(get_outline_write_repo)] +OutlineGatewayDep = Annotated[Gateway, Depends(get_outline_gateway)] +SessionDep = Annotated[AsyncSession, Depends(get_session)] + + +@router.post("/{project_id}/outline") +async def generate_outline( + project_id: uuid.UUID, + body: OutlineGenerateRequest, + request: Request, + project_repo: ProjectRepoDep, + foreshadow_repo: ForeshadowRepoDep, + memory_repos: MemoryReposDep, + outline_repo: OutlineWriteRepoDep, + gateway: OutlineGatewayDep, + session: SessionDep, +) -> OutlineResponse: + """生成大纲并逐章持久化。无凭据 → LLM_UNAVAILABLE;项目不存在 → NOT_FOUND。""" + request_id = getattr(request.state, "request_id", None) + + project = await project_repo.get(STUB_OWNER_ID, project_id) + if project is None: + raise AppError(ErrorCode.NOT_FOUND, f"project not found: {project_id}") + + foreshadow = await foreshadow_repo.list_by_status(project_id, None) + characters = await memory_repos.character.list_for_project(project_id) + world_entities = await memory_repos.world_entity.list_for_project(project_id) + context = build_outline_context( + project=project, + foreshadow=foreshadow, + characters=characters, + world_entities=world_entities, + ) + + log.info( + "outline_generate_start", + project_id=str(project_id), + request_id=request_id, + volume=body.volume, + context_len=len(context), + foreshadow_count=len(foreshadow), + ) + + # run_outline 产结构化大纲(analyst 网关;网关失败上抛,无凭据 → LLM_UNAVAILABLE)。 + result = await run_outline( + outliner_spec, + context=context, + gateway=gateway, + user_id=STUB_OWNER_ID, + project_id=project_id, + ) + + chapters: list[OutlineChapterView] = [] + for chapter in result.chapters: + windows = [w.model_dump() for w in chapter.foreshadow_windows] + await outline_repo.upsert_chapter( + project_id, + volume=body.volume, + chapter_no=chapter.no, + beats=chapter.beats, + foreshadow_windows=windows, + ) + chapters.append( + OutlineChapterView( + no=chapter.no, + volume=body.volume, + beats=list(chapter.beats), + foreshadow_windows=[ForeshadowWindowView(**w) for w in windows], + ) + ) + + # 提交边界:网关 ledger + outline upsert 均只 flush → 端点末尾一次 commit。 + await session.commit() + + log.info( + "outline_generate_done", + project_id=str(project_id), + request_id=request_id, + chapter_count=len(chapters), + ) + return OutlineResponse(chapters=chapters) diff --git a/apps/api/ww_api/routers/projects.py b/apps/api/ww_api/routers/projects.py index 9452a43..0b0f4c9 100644 --- a/apps/api/ww_api/routers/projects.py +++ b/apps/api/ww_api/routers/projects.py @@ -17,7 +17,7 @@ import uuid from collections.abc import AsyncIterator from typing import Annotated -from fastapi import APIRouter, Depends, Request +from fastapi import APIRouter, BackgroundTasks, Depends, Request from fastapi.responses import StreamingResponse from sqlalchemy.ext.asyncio import AsyncSession from ww_core.domain.chapter_repo import ChapterRepo @@ -59,6 +59,7 @@ from ww_api.services.accept_service import ( ) from ww_api.services.credentials import STUB_OWNER_ID from ww_api.services.digest_extraction import extract_digest_facts +from ww_api.services.foreshadow_scan import SessionFactory, run_overdue_scan from ww_api.services.project_deps import ( get_chapter_repo, get_digest_append_repo, @@ -67,6 +68,7 @@ from ww_api.services.project_deps import ( get_project_repo, get_review_gateway, get_review_repo, + get_session_factory, get_writer_gateway, ) @@ -302,14 +304,23 @@ async def accept_chapter( chapter_no: int, body: AcceptRequest, request: Request, + background_tasks: BackgroundTasks, chapter_repo: ChapterRepoDep, digest_repo: DigestRepoDep, review_repo: ReviewRepoDep, gateway: DigestGatewayDep, session: Annotated[AsyncSession, Depends(get_session)], + session_factory: Annotated[SessionFactory, Depends(get_session_factory)], ) -> AcceptResponse: """验收事务 + 冲突 gate(§5.5):gate(事务前)→ 终稿提炼 digest(事务外,R2)→ - 单原子事务(晋升 + digest + 裁决留痕)→ 一次 commit。 + 单原子事务(晋升 + digest + 裁决留痕)→ 一次 commit → 登记**验收后伏笔到期扫描** + BackgroundTask(M3-b/d,§6.2)。 + + 到期扫描接 §5.5 步骤 3 的 `TODO(M3)` 占位:验收提交成功**后**,把 + `current_ch > expected_close_to AND status≠CLOSED` 的伏笔确定性置 OVERDUE。 + 伏笔登记/回收(PARTIAL/CLOSED)的「采纳建议」经作者经 foreshadow 端点显式确认, + 不在验收事务里 AI 静默改库(不变量 #3)。**§7.4 持久性局限**:BackgroundTask 进程内 + 跑、重启丢失——原型可接受(见 `services/foreshadow_scan.py`,不引入 jobs 表)。 """ request_id = getattr(request.state, "request_id", None) # R3:审稿真相从领域表重读(最近一条 chapter_reviews),不依赖 checkpoint。 @@ -350,6 +361,17 @@ async def accept_chapter( latest_review=latest_review, decisions=body.decisions, ) + + # 验收**提交成功后**登记伏笔到期扫描 BackgroundTask(M3-b/d)。current_chapter=刚验收章号; + # 任务在请求 session 关闭后跑,故 run_overdue_scan 自建新 session 并 commit(§7.4 局限可接受)。 + background_tasks.add_task( + run_overdue_scan, + session_factory, + project_id=project_id, + chapter_no=chapter_no, + request_id=request_id, + ) + return AcceptResponse( project_id=project_id, chapter_no=chapter_no, diff --git a/apps/api/ww_api/schemas/foreshadow.py b/apps/api/ww_api/schemas/foreshadow.py new file mode 100644 index 0000000..f221514 --- /dev/null +++ b/apps/api/ww_api/schemas/foreshadow.py @@ -0,0 +1,66 @@ +"""伏笔登记 / 状态变更端点的请求/响应 schema(C3 扩 / ARCH §6.2, §7.2)。 + +snake_case;前端经 OpenAPI 生成 TS 类型消费。改字段 → 前端必须 `pnpm gen:api`。 +登记是**作者显式动作**(不变量 #3:伏笔入库不经 AI 静默写库),到期扫描是验收后的 +确定性纯函数置位(M3-d),二者都不在审稿/写章流水线里。 +""" + +from __future__ import annotations + +from typing import Any + +from pydantic import BaseModel, Field + + +class ForeshadowRegisterRequest(BaseModel): + """POST /projects/:id/foreshadow:作者显式登记一条伏笔(status 落 OPEN)。""" + + code: str = Field(min_length=1, description="伏笔代号,`(project_id, code)` 唯一") + title: str = Field(min_length=1, description="伏笔标题/一句话描述") + planted_at: int | None = Field(default=None, description="埋设章号") + content: str | None = Field(default=None, description="伏笔正文/线索") + expected_close_from: int | None = Field(default=None, description="预期回收窗口起始章") + expected_close_to: int | None = Field( + default=None, description="预期回收窗口结束章(到期判据用)" + ) + importance: str | None = Field(default=None, description="重要度(自由文本,看板用)") + + +class ForeshadowTransitionRequest(BaseModel): + """PATCH /projects/:id/foreshadow/:code:状态机转移 和/或 追加一条进展。 + + 两字段皆可选:`to_status` 走状态机(非法 → VALIDATION 信封);`progress_entry` + append-only 追加到 progress JSONB。二者可同时给(先转移、后追加进展)。 + """ + + to_status: str | None = Field( + default=None, description="目标状态(OPEN/PARTIAL/CLOSED/OVERDUE)" + ) + progress_entry: dict[str, Any] | None = Field( + default=None, description="追加一条进展记录(append-only,不覆盖历史)" + ) + + +class ForeshadowView(BaseModel): + """伏笔账本视图(登记/状态变更/看板共用;snake_case)。形对齐 `ForeshadowLedgerView`。""" + + code: str + title: str + status: str + planted_at: int | None = None + content: str | None = None + expected_close_from: int | None = None + expected_close_to: int | None = None + importance: str | None = None + links: list[dict[str, Any]] = Field(default_factory=list) + progress: list[dict[str, Any]] = Field(default_factory=list) + + +class ForeshadowBoardResponse(BaseModel): + """GET /projects/:id/foreshadow?status=:伏笔看板(四泳道 + OVERDUE 字段齐)。 + + `status` 缺省返回全部;按 `code` 升序(repo `list_by_status` 已排序)。前端按 + `status` 分四泳道(OPEN/PARTIAL/CLOSED/OVERDUE),用 `expected_close_to` 标逾期。 + """ + + foreshadow: list[ForeshadowView] = Field(default_factory=list) diff --git a/apps/api/ww_api/schemas/outline.py b/apps/api/ww_api/schemas/outline.py new file mode 100644 index 0000000..1937303 --- /dev/null +++ b/apps/api/ww_api/schemas/outline.py @@ -0,0 +1,43 @@ +"""大纲生成端点的请求/响应 schema(C3 扩 / ARCH §5.4, §7.2)。 + +snake_case;前端经 OpenAPI 生成 TS 类型消费(改字段 → 前端 `pnpm gen:api`)。 +大纲生成是**作者显式动作**(不变量 #3:outliner `writes=["outline"]` 声明经此端点落地, +非 AI 静默写其它表)。窗口字段供前端 T3.6 渲染伏笔窗口徽标。 +""" + +from __future__ import annotations + +from pydantic import BaseModel, Field + + +class OutlineGenerateRequest(BaseModel): + """POST /projects/:id/outline:触发大纲生成 + 持久化。 + + M3 简单分卷:`volume` 缺省=1(schema 无逐章卷号;本端点把整批章节落到同一卷)。 + """ + + volume: int = Field(default=1, ge=1, description="本批大纲所属卷号(M3 默认全卷 1)") + + +class ForeshadowWindowView(BaseModel): + """单章关联的伏笔回收窗口(snake_case;形对齐 `ww_agents.ForeshadowWindow`)。""" + + code: str + plant_chapter: int | None = None + expected_close_from: int | None = None + expected_close_to: int | None = None + + +class OutlineChapterView(BaseModel): + """单章大纲视图:章号 + 卷 + 节拍 + 伏笔窗口。""" + + no: int + volume: int + beats: list[str] = Field(default_factory=list) + foreshadow_windows: list[ForeshadowWindowView] = Field(default_factory=list) + + +class OutlineResponse(BaseModel): + """大纲生成响应:逐章已持久化的大纲(含窗口,供前端 T3.6)。""" + + chapters: list[OutlineChapterView] = Field(default_factory=list) diff --git a/apps/api/ww_api/services/accept_service.py b/apps/api/ww_api/services/accept_service.py index 5129b5b..17a3f19 100644 --- a/apps/api/ww_api/services/accept_service.py +++ b/apps/api/ww_api/services/accept_service.py @@ -114,9 +114,11 @@ async def run_accept_transaction( ) review_id = updated.id - # 步骤 3(占位):人物 latest_state / 伏笔状态更新——M3 才正式接伏笔表, - # 这里按 §5.5 步骤 3 留占位,不引入 M3 表逻辑(避免越界写未就绪的状态机)。 - # TODO(M3): 按裁决应用 latest_state 变更 + 伏笔登记/到期扫描(§6.2)。 + # 步骤 3:伏笔到期扫描——验收提交**后**经端点登记的 BackgroundTask 跑(M3-b/d, + # `services/foreshadow_scan.run_overdue_scan`:current_ch > expected_close_to 且未 CLOSED + # → 置 OVERDUE,确定性纯函数非 AI,不变量 #3)。放在事务外/提交后是因为它是验收**结果** + # 的副作用、且自建独立 session(请求 session 此时已关闭)。 + # 人物 latest_state 更新仍留后续(M4+):本事务只落晋升 + digest + 裁决留痕。 await session.commit() # type: ignore[attr-defined] # AsyncSession.commit()(fake 同形) diff --git a/apps/api/ww_api/services/foreshadow_scan.py b/apps/api/ww_api/services/foreshadow_scan.py new file mode 100644 index 0000000..a698d1e --- /dev/null +++ b/apps/api/ww_api/services/foreshadow_scan.py @@ -0,0 +1,90 @@ +"""验收后伏笔到期扫描(M3-b/d;ARCH §6.2, §7.4,不变量 #3)。 + +接 §5.5 验收事务后的 `TODO(M3)` 占位:验收**提交成功后**触发一次伏笔到期扫描,把 +`current_ch > expected_close_to AND status≠CLOSED` 的伏笔确定性置 `OVERDUE`(M3-d 纯 +函数,非 Agent;不变量 #3:状态变更是确定性扫描而非 AI 静默写库)。 + +挂载方式(端点):FastAPI `BackgroundTasks` 在 accept 端点登记 `run_overdue_scan`。 +BackgroundTask 在请求-response 发回、请求 session 关闭**之后**才跑——故本函数**自建新 +session**(不复用请求 session)并自己 `commit()`。 + +**持久性局限(§7.4)**:BackgroundTasks 在 API 进程内跑,进程重启/崩溃会丢失未跑完的 +任务——原型可接受,这里把触发/完成/失败都记结构化日志便于查错;**不引入 `jobs` 表** +(那是 M4/T4.1 的工作,本任务不越界)。 + +可测性:扫描逻辑抽成纯 async 函数 `run_overdue_scan(session_factory, ...)`,端点用 +BackgroundTasks 调它;单测直接 `await` 它(注入 fake session 工厂)断言行被置 OVERDUE, +**绝不真起不可控后台线程**。 +""" + +from __future__ import annotations + +import uuid +from collections.abc import Callable +from contextlib import AbstractAsyncContextManager +from typing import Protocol + +import structlog +from sqlalchemy.ext.asyncio import AsyncSession +from ww_core.domain import ForeshadowLedgerView, SqlForeshadowLedgerRepo + +log = structlog.get_logger(__name__) + +# 新建独立 session 的工厂:调用得到一个 async 上下文管理器(`async with` → AsyncSession)。 +SessionFactory = Callable[[], AbstractAsyncContextManager[AsyncSession]] + + +class OverdueScanRepo(Protocol): + """到期扫描对账本 repo 的最小依赖(仅 `scan_overdue`)。""" + + async def scan_overdue( + self, project_id: uuid.UUID, *, current_chapter: int + ) -> list[ForeshadowLedgerView]: ... + + +# repo 工厂:从新 session 造账本 repo。默认建 SQL 实现;测试注入 fake(避免真连 DB)。 +RepoFactory = Callable[[AsyncSession], OverdueScanRepo] + + +def _default_repo_factory(session: AsyncSession) -> OverdueScanRepo: + return SqlForeshadowLedgerRepo(session) + + +async def run_overdue_scan( + session_factory: SessionFactory, + *, + project_id: uuid.UUID, + chapter_no: int, + request_id: str | None = None, + repo_factory: RepoFactory = _default_repo_factory, +) -> int: + """验收后伏笔到期扫描:新建 session → `scan_overdue` 置 OVERDUE → `commit()`。 + + 返回被置 OVERDUE 的伏笔条数(供日志/测试断言)。任何异常被捕获并记错误日志后 + 吞掉——后台任务失败不应冒泡崩进程,也不影响已成功的验收事务(§7.4 局限可接受)。 + `repo_factory` 是可注入缝:默认建 SQL 账本 repo,测试注 fake(直接 await、不联网)。 + """ + try: + async with session_factory() as session: + repo = repo_factory(session) + changed = await repo.scan_overdue(project_id, current_chapter=chapter_no) + if changed: + await session.commit() + log.info( + "foreshadow_overdue_scan", + project_id=str(project_id), + chapter_no=chapter_no, + request_id=request_id, + overdue_count=len(changed), + overdue_codes=[v.code for v in changed], + ) + return len(changed) + except Exception as exc: # noqa: BLE001 — 后台任务边界:记错误、不冒泡崩进程(§7.4)。 + log.error( + "foreshadow_overdue_scan_failed", + project_id=str(project_id), + chapter_no=chapter_no, + request_id=request_id, + error=str(exc), + ) + return 0 diff --git a/apps/api/ww_api/services/outline_context.py b/apps/api/ww_api/services/outline_context.py new file mode 100644 index 0000000..af99a3b --- /dev/null +++ b/apps/api/ww_api/services/outline_context.py @@ -0,0 +1,87 @@ +"""大纲生成的注入上下文组装(确定性序列化文本;ARCH §5.4 outliner reads / §6.2)。 + +把 projects 设定 + 已登记伏笔 + 人物 + 世界观确定性序列化成纯文本喂给 outliner—— +**无时间戳/无 UUID**(缓存前缀稳定,不变量 #9),逐项排序保证同输入同输出(可单测)。 +outliner 据此排出分章节拍 + 伏笔回收窗口(关联本章与伏笔,§6.2)。 + +注:reads=["projects","foreshadow","characters","world_entities"](C6 outliner_spec), +本组装恰好覆盖这四源,不读 digest/outline(排大纲是从设定生成、非续写既有章)。 +""" + +from __future__ import annotations + +from collections.abc import Sequence + +from ww_core.domain import ForeshadowLedgerView, ProjectView +from ww_core.domain.repositories import CharacterView, WorldEntityView + + +def _project_block(project: ProjectView) -> str: + lines = [f"标题:{project.title}"] + if project.genre: + lines.append(f"类型:{project.genre}") + if project.logline: + lines.append(f"一句话简介:{project.logline}") + if project.premise: + lines.append(f"前提:{project.premise}") + if project.theme: + lines.append(f"主题:{project.theme}") + if project.selling_points: + lines.append("卖点:" + "、".join(project.selling_points)) + if project.structure: + lines.append(f"结构:{project.structure}") + return "【作品设定】\n" + "\n".join(lines) + + +def _foreshadow_block(foreshadow: Sequence[ForeshadowLedgerView]) -> str: + if not foreshadow: + return "【已登记伏笔】\n(无)" + rows = sorted(foreshadow, key=lambda f: f.code) + lines = [] + for f in rows: + window = "" + if f.expected_close_from is not None or f.expected_close_to is not None: + lo = f.expected_close_from if f.expected_close_from is not None else "?" + hi = f.expected_close_to if f.expected_close_to is not None else "?" + window = f"(回收窗口 {lo}-{hi})" + plant = f"埋设第{f.planted_at}章 " if f.planted_at is not None else "" + lines.append(f"- [{f.code}] {f.title} {plant}{window}".rstrip()) + return "【已登记伏笔】\n" + "\n".join(lines) + + +def _characters_block(characters: Sequence[CharacterView]) -> str: + if not characters: + return "【人物】\n(无)" + rows = sorted(characters, key=lambda c: c.name) + lines = [] + for c in rows: + role = f"({c.role})" if c.role else "" + motive = f" 动机:{c.motive}" if c.motive else "" + lines.append(f"- {c.name}{role}{motive}".rstrip()) + return "【人物】\n" + "\n".join(lines) + + +def _world_block(entities: Sequence[WorldEntityView]) -> str: + if not entities: + return "【世界观】\n(无)" + rows = sorted(entities, key=lambda e: (e.type, e.name)) + lines = [f"- [{e.type}] {e.name}" for e in rows] + return "【世界观】\n" + "\n".join(lines) + + +def build_outline_context( + *, + project: ProjectView, + foreshadow: Sequence[ForeshadowLedgerView], + characters: Sequence[CharacterView], + world_entities: Sequence[WorldEntityView], +) -> str: + """组装 outliner 注入上下文(确定性文本,纯函数)。""" + return "\n\n".join( + [ + _project_block(project), + _foreshadow_block(foreshadow), + _characters_block(characters), + _world_block(world_entities), + ] + ) diff --git a/apps/api/ww_api/services/project_deps.py b/apps/api/ww_api/services/project_deps.py index e4bc8be..3376b0f 100644 --- a/apps/api/ww_api/services/project_deps.py +++ b/apps/api/ww_api/services/project_deps.py @@ -16,13 +16,15 @@ from openai import AsyncOpenAI from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from ww_config import get_settings +from ww_core.domain import ForeshadowLedgerRepo, SqlForeshadowLedgerRepo from ww_core.domain.chapter_repo import ChapterRepo, SqlChapterRepo from ww_core.domain.digest_repo import DigestAppendRepo, SqlDigestAppendRepo +from ww_core.domain.outline_write_repo import OutlineWriteRepo, SqlOutlineWriteRepo from ww_core.domain.project_repo import ProjectRepo, SqlProjectRepo from ww_core.domain.repositories import MemoryRepos from ww_core.domain.review_repo import ReviewRepo, SqlReviewRepo from ww_core.memory.sql_repositories import sql_memory_repos -from ww_db import get_session +from ww_db import get_session, get_sessionmaker from ww_db.models import User from ww_llm_gateway import ( Gateway, @@ -42,6 +44,7 @@ from ww_api.services.credentials import ( CredentialStore, SqlCredentialStore, ) +from ww_api.services.foreshadow_scan import SessionFactory from ww_api.services.provider_deps import _PROVIDER_BASE_URLS # 单用户 stub 的占位邮箱(多租户化时由真实主体替换)。 @@ -95,6 +98,30 @@ def get_digest_append_repo( return SqlDigestAppendRepo(session) +def get_foreshadow_repo( + session: Annotated[AsyncSession, Depends(get_session)], +) -> ForeshadowLedgerRepo: + """伏笔账本写侧 repo(登记/状态变更端点;只 flush,端点提交)。测试经 override 注 fake。""" + return SqlForeshadowLedgerRepo(session) + + +def get_outline_write_repo( + session: Annotated[AsyncSession, Depends(get_session)], +) -> OutlineWriteRepo: + """大纲写侧 repo(大纲生成端点逐章 upsert;只 flush,端点提交)。测试经 override 注 fake。""" + return SqlOutlineWriteRepo(session) + + +def get_session_factory() -> SessionFactory: + """验收后到期扫描的**独立 session 工厂**缝。 + + BackgroundTask 在请求 session 关闭后才跑,必须自建 session(不复用 `get_session`)。 + 返回的工厂 `()` → `async with` 得一个新 `AsyncSession`。测试经 `app.dependency_overrides` + 注入 fake 工厂(避免真起后台线程/真连 DB)。 + """ + return get_sessionmaker() + + async def build_gateway_for_tier( session: AsyncSession, store: CredentialStore, tier: Tier ) -> Gateway: @@ -160,3 +187,11 @@ async def get_digest_gateway( """验收终稿 digest 提炼(light 档位)的可注入网关缝。测试经 override 注 mock。""" store = SqlCredentialStore(session) return await build_gateway_for_tier(session, store, "light") + + +async def get_outline_gateway( + session: Annotated[AsyncSession, Depends(get_session)], +) -> Gateway: + """大纲生成(analyst 档位)的可注入网关缝。测试经 override 注 mock(产 OutlineResult)。""" + store = SqlCredentialStore(session) + return await build_gateway_for_tier(session, store, "analyst") diff --git a/apps/web/app/projects/[id]/foreshadow/page.tsx b/apps/web/app/projects/[id]/foreshadow/page.tsx new file mode 100644 index 0000000..832097c --- /dev/null +++ b/apps/web/app/projects/[id]/foreshadow/page.tsx @@ -0,0 +1,23 @@ +import { notFound } from "next/navigation"; + +import { ForeshadowBoard } from "@/components/foreshadow/ForeshadowBoard"; +import { fetchForeshadow, fetchProject } from "@/lib/api/server"; + +interface PageProps { + params: Promise<{ id: string }>; +} + +// 伏笔看板页(UX §6.8)。Server Component 取项目 + 全量伏笔(GET .../foreshadow); +// ForeshadowBoard(Client)按 status 分四泳道并承载登记/状态变更交互。 +export default async function ForeshadowPage({ params }: PageProps) { + const { id } = await params; + try { + const project = await fetchProject(id); + const board = await fetchForeshadow(id); + return ( + + ); + } catch { + notFound(); + } +} diff --git a/apps/web/app/projects/[id]/outline/page.tsx b/apps/web/app/projects/[id]/outline/page.tsx new file mode 100644 index 0000000..9044ed9 --- /dev/null +++ b/apps/web/app/projects/[id]/outline/page.tsx @@ -0,0 +1,20 @@ +import { notFound } from "next/navigation"; + +import { OutlineEditor } from "@/components/outline/OutlineEditor"; +import { fetchProject } from "@/lib/api/server"; + +interface PageProps { + params: Promise<{ id: string }>; +} + +// 大纲编辑器页(UX §6.7)。Server Component 取项目;OutlineEditor(Client)按需 +// 触发 POST .../outline 生成大纲(M3 无 GET 大纲端点,进页空、生成后填充)。 +export default async function OutlinePage({ params }: PageProps) { + const { id } = await params; + try { + const project = await fetchProject(id); + return ; + } catch { + notFound(); + } +} diff --git a/apps/web/components/AppShell.tsx b/apps/web/components/AppShell.tsx index 2a29826..ef2afd5 100644 --- a/apps/web/components/AppShell.tsx +++ b/apps/web/components/AppShell.tsx @@ -8,10 +8,20 @@ interface AppShellProps { // 顶栏右侧的上下文信息(如作品名 / 进度)。 title?: string; subtitle?: string; + // 项目内页给出 projectId → 左导航展开项目级入口(大纲/伏笔/审稿,UX §3.1)。 + projectId?: string; + // 当前激活的项目级入口键(用于高亮)。 + activeNav?: "write" | "outline" | "foreshadow" | "review"; } // 全局外壳:顶栏(64px) + 左导航 + 主区(UX §5.1)。 -export function AppShell({ children, title, subtitle }: AppShellProps) { +export function AppShell({ + children, + title, + subtitle, + projectId, + activeNav, +}: AppShellProps) { return (
@@ -38,7 +48,7 @@ export function AppShell({ children, title, subtitle }: AppShellProps) {
- +
{children}
diff --git a/apps/web/components/LeftNav.tsx b/apps/web/components/LeftNav.tsx index 4a1a2fe..376f17f 100644 --- a/apps/web/components/LeftNav.tsx +++ b/apps/web/components/LeftNav.tsx @@ -3,64 +3,126 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; +export type ActiveNav = "write" | "outline" | "foreshadow" | "review"; + +interface LeftNavProps { + // 项目内页传 projectId → 展开项目级入口(大纲/伏笔/审稿,UX §3.1)。 + projectId?: string; + activeNav?: ActiveNav; +} + interface NavItem { href: string; label: string; - // M1 仅作品库 + 设置可达;其余为后续里程碑占位(禁用)。 enabled: boolean; + // 项目级入口的激活键(与 activeNav 比对高亮)。 + key?: ActiveNav; } -const NAV_ITEMS: NavItem[] = [ +const GLOBAL_ITEMS: NavItem[] = [ { href: "/", label: "作品库", enabled: true }, { href: "/settings/providers", label: "设置", enabled: true }, - { href: "#outline", label: "大纲", enabled: false }, - { href: "#codex", label: "设定库", enabled: false }, - { href: "#foreshadow", label: "伏笔", enabled: false }, - { href: "#review", label: "审稿", enabled: false }, - { href: "#style", label: "文风", enabled: false }, ]; -// 左导航:当前项朱砂竖条 + 浅晕底(UX §5.1)。 -export function LeftNav() { +function projectItems(projectId: string): NavItem[] { + return [ + { + href: `/projects/${projectId}/write`, + label: "写作", + enabled: true, + key: "write", + }, + { + href: `/projects/${projectId}/outline`, + label: "大纲", + enabled: true, + key: "outline", + }, + { + href: `/projects/${projectId}/foreshadow`, + label: "伏笔", + enabled: true, + key: "foreshadow", + }, + { + href: `/projects/${projectId}/review`, + label: "审稿", + enabled: true, + key: "review", + }, + { href: "#codex", label: "设定库", enabled: false }, + { href: "#style", label: "文风", enabled: false }, + ]; +} + +// 左导航:当前项朱砂竖条 + 浅晕底(UX §5.1 / §3.1)。 +export function LeftNav({ projectId, activeNav }: LeftNavProps) { const pathname = usePathname(); + const scoped = projectId ? projectItems(projectId) : []; + return ( ); } + +function NavLink({ item, active }: { item: NavItem; active: boolean }) { + if (!item.enabled) { + return ( +
  • + + {item.label} + +
  • + ); + } + return ( +
  • + + {item.label} + +
  • + ); +} diff --git a/apps/web/components/foreshadow/ForeshadowBoard.tsx b/apps/web/components/foreshadow/ForeshadowBoard.tsx new file mode 100644 index 0000000..a6444ae --- /dev/null +++ b/apps/web/components/foreshadow/ForeshadowBoard.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { useMemo } from "react"; + +import { AppShell } from "@/components/AppShell"; +import type { ForeshadowView, ProjectResponse } from "@/lib/api/types"; +import { + LANES, + groupByStatus, + type ForeshadowStatus, +} from "@/lib/foreshadow/board"; +import { useForeshadow } from "@/lib/foreshadow/useForeshadow"; +import { KanbanColumn } from "./KanbanColumn"; +import { RegisterForm } from "./RegisterForm"; + +interface ForeshadowBoardProps { + project: ProjectResponse; + initialItems: ForeshadowView[]; +} + +// 伏笔看板主体(UX §6.8):四泳道 + 登记/状态变更交互。 +// RSC 取全量种入;Client 承载登记/转移(乐观 + 回滚 + Toast)。 +export function ForeshadowBoard({ + project, + initialItems, +}: ForeshadowBoardProps) { + const { items, busy, register, transition } = useForeshadow(initialItems); + const lanes = useMemo(() => groupByStatus(items), [items]); + + const onTransition = ( + code: string, + toStatus: ForeshadowStatus | null, + progressNote: string, + ): void => { + void transition(code, { + projectId: project.id, + toStatus, + progressEntry: progressNote ? { note: progressNote } : null, + }); + }; + + return ( + +
    +
    +

    伏笔看板

    + +
    +
    + {LANES.map((status) => ( + + ))} +
    +
    +
    + ); +} diff --git a/apps/web/components/foreshadow/ForeshadowCard.tsx b/apps/web/components/foreshadow/ForeshadowCard.tsx new file mode 100644 index 0000000..1648b56 --- /dev/null +++ b/apps/web/components/foreshadow/ForeshadowCard.tsx @@ -0,0 +1,133 @@ +"use client"; + +import { useState } from "react"; + +import type { ForeshadowView } from "@/lib/api/types"; +import type { ForeshadowStatus } from "@/lib/foreshadow/board"; + +interface ForeshadowCardProps { + item: ForeshadowView; + overdue: boolean; + busy: boolean; + onTransition: ( + code: string, + toStatus: ForeshadowStatus | null, + progressNote: string, + ) => void; +} + +const NEXT_STATUS: Record = { + OPEN: ["PARTIAL", "CLOSED"], + PARTIAL: ["CLOSED"], + OVERDUE: ["PARTIAL", "CLOSED"], + CLOSED: [], +}; + +// 伏笔卡(UX §6.8):code/title/回收窗口/importance/progress 时间线 + 状态变更/加进展。 +// OVERDUE 用琥珀强调(不单靠色:加 ⚠ 图标 + 文案,a11y §10)。 +export function ForeshadowCard({ + item, + overdue, + busy, + onTransition, +}: ForeshadowCardProps) { + const [note, setNote] = useState(""); + const transitions = NEXT_STATUS[item.status] ?? []; + const windowText = recoveryWindow(item); + + return ( +
  • +
    + {item.code} + {overdue ? ( + + ⚠ 逾期 + + ) : null} +
    +

    {item.title}

    + +
    + {typeof item.planted_at === "number" ? ( +
    埋于 {item.planted_at} 章
    + ) : null} + {windowText ?
    窗口 {windowText}
    : null} + {item.importance ?
    {item.importance}
    : null} +
    + + {item.progress && item.progress.length > 0 ? ( +
      + {item.progress.map((p, i) => ( +
    • · {progressLabel(p)}
    • + ))} +
    + ) : null} + + {transitions.length > 0 || item.status !== "CLOSED" ? ( +
    + {transitions.length > 0 ? ( +
    + {transitions.map((to) => ( + + ))} +
    + ) : null} + {item.status !== "CLOSED" ? ( +
    + + setNote(e.target.value)} + placeholder="加进展(如:23章发光)" + className="min-w-0 flex-1 rounded border border-line bg-bg px-2 py-0.5 text-xs text-ink focus:border-cinnabar focus:outline-none" + /> + +
    + ) : null} +
    + ) : null} +
  • + ); +} + +function recoveryWindow(item: ForeshadowView): string | null { + const from = item.expected_close_from; + const to = item.expected_close_to; + if (typeof from === "number" && typeof to === "number") return `${from}-${to}`; + if (typeof to === "number") return `≤${to}`; + if (typeof from === "number") return `≥${from}`; + return null; +} + +function progressLabel(entry: Record): string { + const chapter = entry["chapter"]; + const note = entry["note"]; + const noteStr = typeof note === "string" ? note : JSON.stringify(entry); + return typeof chapter === "number" ? `${chapter}章:${noteStr}` : noteStr; +} diff --git a/apps/web/components/foreshadow/KanbanColumn.tsx b/apps/web/components/foreshadow/KanbanColumn.tsx new file mode 100644 index 0000000..0f3700a --- /dev/null +++ b/apps/web/components/foreshadow/KanbanColumn.tsx @@ -0,0 +1,61 @@ +"use client"; + +import type { ForeshadowView } from "@/lib/api/types"; +import type { ForeshadowStatus } from "@/lib/foreshadow/board"; +import { ForeshadowCard } from "./ForeshadowCard"; + +interface KanbanColumnProps { + status: ForeshadowStatus; + items: ForeshadowView[]; + busy: boolean; + onTransition: ( + code: string, + toStatus: ForeshadowStatus | null, + progressNote: string, + ) => void; +} + +// 单个状态泳道(UX §6.8)。OVERDUE 列整列琥珀强调(表头 + 卡片)。 +export function KanbanColumn({ + status, + items, + busy, + onTransition, +}: KanbanColumnProps) { + const overdue = status === "OVERDUE"; + return ( +
    +
    + + {overdue ? "⚠ " : ""} + {status} + + {items.length} +
    +
      + {items.length === 0 ? ( +
    • + ) : ( + items.map((item) => ( + + )) + )} +
    +
    + ); +} diff --git a/apps/web/components/foreshadow/RegisterForm.tsx b/apps/web/components/foreshadow/RegisterForm.tsx new file mode 100644 index 0000000..4d074c9 --- /dev/null +++ b/apps/web/components/foreshadow/RegisterForm.tsx @@ -0,0 +1,185 @@ +"use client"; + +import { useState } from "react"; + +import type { RegisterInput } from "@/lib/foreshadow/board"; + +interface RegisterFormProps { + projectId: string; + busy: boolean; + onRegister: (input: RegisterInput) => Promise; +} + +const EMPTY = { + code: "", + title: "", + plantedAt: "", + content: "", + expectedCloseFrom: "", + expectedCloseTo: "", + importance: "", +}; + +// 登记新伏笔(UX §6.8)。受控表单 → 组装 RegisterInput;成功后清空。 +export function RegisterForm({ + projectId, + busy, + onRegister, +}: RegisterFormProps) { + const [open, setOpen] = useState(false); + const [form, setForm] = useState({ ...EMPTY }); + + const set = (key: keyof typeof EMPTY, value: string): void => + setForm((prev) => ({ ...prev, [key]: value })); + + const submit = async (): Promise => { + const ok = await onRegister({ + projectId, + code: form.code, + title: form.title, + plantedAt: toInt(form.plantedAt), + content: form.content || null, + expectedCloseFrom: toInt(form.expectedCloseFrom), + expectedCloseTo: toInt(form.expectedCloseTo), + importance: form.importance || null, + }); + if (ok) { + setForm({ ...EMPTY }); + setOpen(false); + } + }; + + if (!open) { + return ( + + ); + } + + const canSubmit = + form.code.trim().length > 0 && form.title.trim().length > 0 && !busy; + + return ( +
    { + e.preventDefault(); + void submit(); + }} + className="rounded border border-line bg-panel p-4" + > +
    + + set("code", e.target.value)} + className={inputCls} + /> + + + set("title", e.target.value)} + className={inputCls} + /> + + + set("plantedAt", e.target.value)} + className={inputCls} + /> + + + set("expectedCloseFrom", e.target.value)} + className={inputCls} + /> + + + set("expectedCloseTo", e.target.value)} + className={inputCls} + /> + + + set("importance", e.target.value)} + placeholder="主线/支线" + className={inputCls} + /> + + + set("content", e.target.value)} + className={inputCls} + /> + +
    +
    + + +
    +
    + ); +} + +const inputCls = + "w-full rounded border border-line bg-bg px-2 py-1 text-sm text-ink focus:border-cinnabar focus:outline-none"; + +interface FieldProps { + label: string; + id: string; + span2?: boolean; + span3?: boolean; + children: React.ReactNode; +} + +function Field({ label, id, span2, span3, children }: FieldProps) { + const span = span3 ? "col-span-2 sm:col-span-3" : span2 ? "col-span-2" : ""; + return ( +
    + +
    {children}
    +
    + ); +} + +function toInt(raw: string): number | null { + const trimmed = raw.trim(); + if (trimmed.length === 0) return null; + const n = Number.parseInt(trimmed, 10); + return Number.isInteger(n) ? n : null; +} diff --git a/apps/web/components/outline/OutlineChapterRow.tsx b/apps/web/components/outline/OutlineChapterRow.tsx new file mode 100644 index 0000000..675fb5d --- /dev/null +++ b/apps/web/components/outline/OutlineChapterRow.tsx @@ -0,0 +1,51 @@ +"use client"; + +import type { OutlineChapterView } from "@/lib/api/types"; +import { + isCloseWindow, + windowBadgeLabel, +} from "@/lib/outline/outline"; + +interface OutlineChapterRowProps { + chapter: OutlineChapterView; +} + +// 单章大纲行(UX §6.7):章号 + beats + 伏笔窗口徽标(⚑)。 +// 接近回收窗口的徽标用琥珀 + 「可回收」提示(a11y:图标 + 文案,不单靠色)。 +export function OutlineChapterRow({ chapter }: OutlineChapterRowProps) { + const windows = chapter.foreshadow_windows ?? []; + return ( +
  • +
    + + 第 {chapter.no} 章 + + {windows.map((w) => { + const close = isCloseWindow(chapter, w); + return ( + + {windowBadgeLabel(w)} + {close ? " · 可回收" : ""} + + ); + })} +
    + {chapter.beats && chapter.beats.length > 0 ? ( +

    + beats: + {chapter.beats.join(" / ")} +

    + ) : ( +

    (无节拍)

    + )} +
  • + ); +} diff --git a/apps/web/components/outline/OutlineEditor.tsx b/apps/web/components/outline/OutlineEditor.tsx new file mode 100644 index 0000000..104e957 --- /dev/null +++ b/apps/web/components/outline/OutlineEditor.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { useMemo, useState } from "react"; + +import { AppShell } from "@/components/AppShell"; +import type { OutlineChapterView, ProjectResponse } from "@/lib/api/types"; +import { groupByVolume } from "@/lib/outline/outline"; +import { useOutline } from "@/lib/outline/useOutline"; +import { OutlineChapterRow } from "./OutlineChapterRow"; + +interface OutlineEditorProps { + project: ProjectResponse; + initialChapters: OutlineChapterView[]; +} + +// 大纲编辑器主体(UX §6.7)。RSC 种入已存大纲;Client 承载生成(POST .../outline)。 +// 无凭据 → 503 LLM_UNAVAILABLE 经 error 检出、引导去设置。 +export function OutlineEditor({ + project, + initialChapters, +}: OutlineEditorProps) { + const { chapters, status, error, generate } = useOutline(initialChapters); + const [volume, setVolume] = useState(1); + const volumes = useMemo(() => groupByVolume(chapters), [chapters]); + const generating = status === "generating"; + + return ( + +
    +
    +

    大纲

    + + setVolume(Math.max(1, Number(e.target.value) || 1))} + className="w-16 rounded border border-line bg-bg px-2 py-1 text-sm text-ink focus:border-cinnabar focus:outline-none" + /> + +
    + + {error ? ( +

    + 大纲生成失败({error.code}):{error.message} + {error.code === "LLM_UNAVAILABLE" ? ( + <> + {" "} + + 去设置提供商 + + + ) : null} +

    + ) : null} + +
    + {volumes.length === 0 ? ( +

    + 暂无大纲。点「✦ AI 排大纲」生成逐章节拍与伏笔窗口。 +

    + ) : ( + volumes.map((group) => ( +
    +

    + 卷 {group.volume} +

    +
      + {group.chapters.map((ch) => ( + + ))} +
    +
    + )) + )} +
    +
    +
    + ); +} diff --git a/apps/web/components/review/BeatMap.tsx b/apps/web/components/review/BeatMap.tsx new file mode 100644 index 0000000..a85210d --- /dev/null +++ b/apps/web/components/review/BeatMap.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { toBeatCells, toSparkline } from "@/lib/pace/beatmap"; + +interface BeatMapProps { + beatMap: number[]; +} + +// 爽点节拍图(UX §6.4):beat_map(int 序列) → ▁▃▅ 柱状条。 +// 渲染为定高柱(CSS height%),同时给文本 sparkline 作无障碍标签(a11y §10)。 +// 静态高度,无动画 → 天然尊重 prefers-reduced-motion。 +export function BeatMap({ beatMap }: BeatMapProps) { + const cells = toBeatCells(beatMap); + if (cells.length === 0) { + return 无节拍数据; + } + const spark = toSparkline(beatMap); + return ( +
    + {cells.map((cell, i) => ( +
    + ))} +
    + ); +} diff --git a/apps/web/components/review/ForeshadowSuggestions.tsx b/apps/web/components/review/ForeshadowSuggestions.tsx new file mode 100644 index 0000000..df9ba51 --- /dev/null +++ b/apps/web/components/review/ForeshadowSuggestions.tsx @@ -0,0 +1,58 @@ +"use client"; + +import type { ForeshadowSuggestion } from "@/lib/review/sse"; + +interface ForeshadowSuggestionsProps { + suggestions: ForeshadowSuggestion[]; + // 审项是否未完成(incomplete)。 + incomplete: boolean; +} + +// 伏笔建议区(UX §6.4 ②):planted「新埋待确认」/ resolved「疑似回收」,只读建议。 +// 不静默写库(不变量#3):仅展示,登记/回收经伏笔看板显式确认。 +export function ForeshadowSuggestions({ + suggestions, + incomplete, +}: ForeshadowSuggestionsProps) { + return ( +
    +

    + 伏笔 (foreshadow-analyst) +

    + {incomplete ? ( +

    ◌ 未完成

    + ) : suggestions.length === 0 ? ( +

    无伏笔建议。

    + ) : ( +
      + {suggestions.map((s, i) => ( +
    • + + {s.kind === "resolved" ? "⚑ 疑似回收" : "+ 新埋待确认"} + + {s.code ? ( + {s.code} + ) : null} +

      {s.title}

      + {s.where ? ( + {s.where} + ) : null} + {s.note ? ( +

      {s.note}

      + ) : null} +
    • + ))} +
    + )} +
    + ); +} diff --git a/apps/web/components/review/PacePanel.tsx b/apps/web/components/review/PacePanel.tsx new file mode 100644 index 0000000..13be97d --- /dev/null +++ b/apps/web/components/review/PacePanel.tsx @@ -0,0 +1,56 @@ +"use client"; + +import type { PaceReport } from "@/lib/review/sse"; +import { BeatMap } from "./BeatMap"; + +interface PacePanelProps { + pace: PaceReport | null; + incomplete: boolean; +} + +// 节奏报告区(UX §6.4 ④):节拍图 ▁▃▅ + 注水段列表 + 章末钩子徽标。只读建议。 +export function PacePanel({ pace, incomplete }: PacePanelProps) { + return ( +
    +

    + 节奏 (pace-checker) +

    + {incomplete ? ( +

    ◌ 未完成

    + ) : pace === null ? ( +

    无节奏报告。

    + ) : ( +
    +
    +

    爽点节拍图

    + +
    + +

    + 章末钩子: + {pace.hook ? ( + ✓ 有 + ) : ( + ⚠ 无(建议补悬念) + )} +

    + + {pace.water.length > 0 ? ( +
    +

    ⚠ 注水段

    +
      + {pace.water.map((w, i) => ( +
    • + {w.where}:{w.reason} +
    • + ))} +
    +
    + ) : ( +

    ✓ 无明显注水

    + )} +
    + )} +
    + ); +} diff --git a/apps/web/components/review/ReviewReport.tsx b/apps/web/components/review/ReviewReport.tsx index 5481bdf..c978c9e 100644 --- a/apps/web/components/review/ReviewReport.tsx +++ b/apps/web/components/review/ReviewReport.tsx @@ -13,13 +13,19 @@ import { type DecisionDraft, type Verdict, } from "@/lib/review/decisions"; -import { normalizeConflicts } from "@/lib/review/history"; +import { + normalizeConflicts, + normalizeForeshadowSug, + normalizePace, +} from "@/lib/review/history"; import { useAccept } from "@/lib/review/useAccept"; import { useReviewStream } from "@/lib/review/useReviewStream"; import type { ReviewConflict } from "@/lib/review/sse"; import { AcceptPanel } from "./AcceptPanel"; import { AnnotatedText } from "./AnnotatedText"; import { ConflictCard } from "./ConflictCard"; +import { ForeshadowSuggestions } from "./ForeshadowSuggestions"; +import { PacePanel } from "./PacePanel"; interface ReviewReportProps { project: ProjectResponse; @@ -48,19 +54,41 @@ export function ReviewReport({ const [missing, setMissing] = useState>(new Set()); const seededRef = useRef(false); - // 进页一次性把历史 conflicts 种入流状态(无需重审即可裁决)。 + // 进页一次性把历史留痕(冲突 + 伏笔建议 + 节奏)种入流状态(无需重审即可裁决/查看)。 const seededConflicts = useMemo( () => normalizeConflicts(initialReview), [initialReview], ); + const seededForeshadow = useMemo( + () => normalizeForeshadowSug(initialReview), + [initialReview], + ); + const seededPace = useMemo( + () => normalizePace(initialReview), + [initialReview], + ); useEffect(() => { if (seededRef.current) return; seededRef.current = true; - if (seededConflicts.length > 0) review.seed(seededConflicts); - }, [review, seededConflicts]); + const hasSeed = + seededConflicts.length > 0 || + seededForeshadow.length > 0 || + seededPace !== null; + if (hasSeed) { + review.seed({ + conflicts: seededConflicts, + foreshadow: seededForeshadow, + pace: seededPace, + }); + } + }, [review, seededConflicts, seededForeshadow, seededPace]); // 当前生效冲突 = 流状态(重审后即时更新,否则种入的历史)。 const conflicts: ReviewConflict[] = review.state.conflicts; + const foreshadow = review.state.foreshadow; + const pace = review.state.pace; + const sectionStatus = (name: string): string | undefined => + review.state.sections.find((s) => s.name === name)?.status; // 重审完成(done 边沿)→ 重置裁决草稿对齐新冲突集(冲突可能整组变化)。 const conflictCount = conflicts.length; @@ -127,6 +155,8 @@ export function ReviewReport({
    {/* 左:终稿正文 + 就地标注 */} @@ -240,6 +270,17 @@ export function ReviewReport({ )} +
    + + +
    +
    @@ -137,6 +139,18 @@ function Toolbar({ 字数 {wordCount.toLocaleString()} + + 大纲 + + + 伏笔 + expected_close_to AND status≠CLOSED` 的伏笔确定性置 OVERDUE。 + * 伏笔登记/回收(PARTIAL/CLOSED)的「采纳建议」经作者经 foreshadow 端点显式确认, + * 不在验收事务里 AI 静默改库(不变量 #3)。**§7.4 持久性局限**:BackgroundTask 进程内 + * 跑、重启丢失——原型可接受(见 `services/foreshadow_scan.py`,不引入 jobs 表)。 */ post: operations["accept_chapter_projects__project_id__chapters__chapter_no__accept_post"]; delete?: never; @@ -178,6 +185,75 @@ export interface paths { patch?: never; trace?: never; }; + "/projects/{project_id}/foreshadow": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List Foreshadow + * @description 伏笔看板:按 `status` 过滤(缺省=全部),按 code 升序。 + * + * `status` 非法(不在 OPEN/PARTIAL/CLOSED/OVERDUE)→ VALIDATION 信封。四泳道前端 + * 据 `status` 分组;OVERDUE 泳道 + 逾期标记用 `expected_close_to`(看板字段已齐)。 + */ + get: operations["list_foreshadow_projects__project_id__foreshadow_get"]; + put?: never; + /** + * Register Foreshadow + * @description 登记一条伏笔(status=OPEN)。重复 `code` → DB 唯一约束冲突 → VALIDATION 信封。 + */ + post: operations["register_foreshadow_projects__project_id__foreshadow_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/projects/{project_id}/foreshadow/{code}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + /** + * Update Foreshadow + * @description 状态机转移 和/或 追加进展。非法转移 → VALIDATION 信封;不存在 → NOT_FOUND。 + * + * `to_status` 与 `progress_entry` 皆可选;两者都缺 → VALIDATION(无操作)。先转移、后追加。 + */ + patch: operations["update_foreshadow_projects__project_id__foreshadow__code__patch"]; + trace?: never; + }; + "/projects/{project_id}/outline": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Generate Outline + * @description 生成大纲并逐章持久化。无凭据 → LLM_UNAVAILABLE;项目不存在 → NOT_FOUND。 + */ + post: operations["generate_outline_projects__project_id__outline_post"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/settings/providers": { parameters: { query?: never; @@ -338,11 +414,164 @@ export interface components { /** Text */ text: string; }; + /** + * ForeshadowBoardResponse + * @description GET /projects/:id/foreshadow?status=:伏笔看板(四泳道 + OVERDUE 字段齐)。 + * + * `status` 缺省返回全部;按 `code` 升序(repo `list_by_status` 已排序)。前端按 + * `status` 分四泳道(OPEN/PARTIAL/CLOSED/OVERDUE),用 `expected_close_to` 标逾期。 + */ + ForeshadowBoardResponse: { + /** Foreshadow */ + foreshadow?: components["schemas"]["ForeshadowView"][]; + }; + /** + * ForeshadowRegisterRequest + * @description POST /projects/:id/foreshadow:作者显式登记一条伏笔(status 落 OPEN)。 + */ + ForeshadowRegisterRequest: { + /** + * Code + * @description 伏笔代号,`(project_id, code)` 唯一 + */ + code: string; + /** + * Title + * @description 伏笔标题/一句话描述 + */ + title: string; + /** + * Planted At + * @description 埋设章号 + */ + planted_at?: number | null; + /** + * Content + * @description 伏笔正文/线索 + */ + content?: string | null; + /** + * Expected Close From + * @description 预期回收窗口起始章 + */ + expected_close_from?: number | null; + /** + * Expected Close To + * @description 预期回收窗口结束章(到期判据用) + */ + expected_close_to?: number | null; + /** + * Importance + * @description 重要度(自由文本,看板用) + */ + importance?: string | null; + }; + /** + * ForeshadowTransitionRequest + * @description PATCH /projects/:id/foreshadow/:code:状态机转移 和/或 追加一条进展。 + * + * 两字段皆可选:`to_status` 走状态机(非法 → VALIDATION 信封);`progress_entry` + * append-only 追加到 progress JSONB。二者可同时给(先转移、后追加进展)。 + */ + ForeshadowTransitionRequest: { + /** + * To Status + * @description 目标状态(OPEN/PARTIAL/CLOSED/OVERDUE) + */ + to_status?: string | null; + /** + * Progress Entry + * @description 追加一条进展记录(append-only,不覆盖历史) + */ + progress_entry?: { + [key: string]: unknown; + } | null; + }; + /** + * ForeshadowView + * @description 伏笔账本视图(登记/状态变更/看板共用;snake_case)。形对齐 `ForeshadowLedgerView`。 + */ + ForeshadowView: { + /** Code */ + code: string; + /** Title */ + title: string; + /** Status */ + status: string; + /** Planted At */ + planted_at?: number | null; + /** Content */ + content?: string | null; + /** Expected Close From */ + expected_close_from?: number | null; + /** Expected Close To */ + expected_close_to?: number | null; + /** Importance */ + importance?: string | null; + /** Links */ + links?: { + [key: string]: unknown; + }[]; + /** Progress */ + progress?: { + [key: string]: unknown; + }[]; + }; + /** + * ForeshadowWindowView + * @description 单章关联的伏笔回收窗口(snake_case;形对齐 `ww_agents.ForeshadowWindow`)。 + */ + ForeshadowWindowView: { + /** Code */ + code: string; + /** Plant Chapter */ + plant_chapter?: number | null; + /** Expected Close From */ + expected_close_from?: number | null; + /** Expected Close To */ + expected_close_to?: number | null; + }; /** HTTPValidationError */ HTTPValidationError: { /** Detail */ detail?: components["schemas"]["ValidationError"][]; }; + /** + * OutlineChapterView + * @description 单章大纲视图:章号 + 卷 + 节拍 + 伏笔窗口。 + */ + OutlineChapterView: { + /** No */ + no: number; + /** Volume */ + volume: number; + /** Beats */ + beats?: string[]; + /** Foreshadow Windows */ + foreshadow_windows?: components["schemas"]["ForeshadowWindowView"][]; + }; + /** + * OutlineGenerateRequest + * @description POST /projects/:id/outline:触发大纲生成 + 持久化。 + * + * M3 简单分卷:`volume` 缺省=1(schema 无逐章卷号;本端点把整批章节落到同一卷)。 + */ + OutlineGenerateRequest: { + /** + * Volume + * @description 本批大纲所属卷号(M3 默认全卷 1) + * @default 1 + */ + volume: number; + }; + /** + * OutlineResponse + * @description 大纲生成响应:逐章已持久化的大纲(含窗口,供前端 T3.6)。 + */ + OutlineResponse: { + /** Chapters */ + chapters?: components["schemas"]["OutlineChapterView"][]; + }; /** * ProjectCreateRequest * @description POST /projects:立项向导字段(owner_id 由后端补 stub,不入参)。 @@ -898,6 +1127,145 @@ export interface operations { }; }; }; + list_foreshadow_projects__project_id__foreshadow_get: { + parameters: { + query?: { + status?: string | null; + }; + header?: never; + path: { + project_id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ForeshadowBoardResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + register_foreshadow_projects__project_id__foreshadow_post: { + parameters: { + query?: never; + header?: never; + path: { + project_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ForeshadowRegisterRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ForeshadowView"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + update_foreshadow_projects__project_id__foreshadow__code__patch: { + parameters: { + query?: never; + header?: never; + path: { + project_id: string; + code: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ForeshadowTransitionRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ForeshadowView"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; + generate_outline_projects__project_id__outline_post: { + parameters: { + query?: never; + header?: never; + path: { + project_id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OutlineGenerateRequest"]; + }; + }; + responses: { + /** @description Successful Response */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OutlineResponse"]; + }; + }; + /** @description Validation Error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["HTTPValidationError"]; + }; + }; + }; + }; list_providers_settings_providers_get: { parameters: { query?: never; diff --git a/apps/web/lib/api/server.ts b/apps/web/lib/api/server.ts index 5b9d0f3..264b5d0 100644 --- a/apps/web/lib/api/server.ts +++ b/apps/web/lib/api/server.ts @@ -1,5 +1,6 @@ import { serverApiBase } from "./config"; import type { + ForeshadowBoardResponse, ProjectListResponse, ProjectResponse, ProvidersResponse, @@ -35,3 +36,10 @@ export async function fetchReviews( `/projects/${projectId}/chapters/${chapterNo}/reviews`, ); } + +// 伏笔看板(全量,前端按 status 分四泳道)。 +export async function fetchForeshadow( + projectId: string, +): Promise { + return getJson(`/projects/${projectId}/foreshadow`); +} diff --git a/apps/web/lib/api/types.ts b/apps/web/lib/api/types.ts index cfcab66..30ed013 100644 --- a/apps/web/lib/api/types.ts +++ b/apps/web/lib/api/types.ts @@ -25,3 +25,18 @@ export type ReviewHistoryResponse = export type ConflictDecision = components["schemas"]["ConflictDecision"]; export type AcceptRequest = components["schemas"]["AcceptRequest"]; export type AcceptResponse = components["schemas"]["AcceptResponse"]; + +// M3 伏笔 + 大纲(C3 扩 T3.2/T3.5)。 +export type ForeshadowView = components["schemas"]["ForeshadowView"]; +export type ForeshadowBoardResponse = + components["schemas"]["ForeshadowBoardResponse"]; +export type ForeshadowRegisterRequest = + components["schemas"]["ForeshadowRegisterRequest"]; +export type ForeshadowTransitionRequest = + components["schemas"]["ForeshadowTransitionRequest"]; +export type ForeshadowWindowView = + components["schemas"]["ForeshadowWindowView"]; +export type OutlineChapterView = components["schemas"]["OutlineChapterView"]; +export type OutlineGenerateRequest = + components["schemas"]["OutlineGenerateRequest"]; +export type OutlineResponse = components["schemas"]["OutlineResponse"]; diff --git a/apps/web/lib/foreshadow/board.test.ts b/apps/web/lib/foreshadow/board.test.ts new file mode 100644 index 0000000..0c67403 --- /dev/null +++ b/apps/web/lib/foreshadow/board.test.ts @@ -0,0 +1,134 @@ +import { describe, expect, it } from "vitest"; + +import type { ForeshadowView } from "@/lib/api/types"; +import { + buildRegisterRequest, + buildTransitionRequest, + extractReason, + groupByStatus, + isWindowApproaching, + validationReasonMessage, +} from "./board"; + +const view = (over: Partial): ForeshadowView => ({ + code: "F-001", + title: "线索", + status: "OPEN", + ...over, +}); + +describe("groupByStatus", () => { + it("splits items into four lanes preserving order", () => { + const lanes = groupByStatus([ + view({ code: "F-001", status: "OPEN" }), + view({ code: "F-002", status: "OVERDUE" }), + view({ code: "F-003", status: "OPEN" }), + view({ code: "F-004", status: "CLOSED" }), + view({ code: "F-005", status: "PARTIAL" }), + ]); + expect(lanes.OPEN.map((v) => v.code)).toEqual(["F-001", "F-003"]); + expect(lanes.OVERDUE.map((v) => v.code)).toEqual(["F-002"]); + expect(lanes.CLOSED.map((v) => v.code)).toEqual(["F-004"]); + expect(lanes.PARTIAL.map((v) => v.code)).toEqual(["F-005"]); + }); + + it("falls back unknown status to OPEN and handles undefined", () => { + const lanes = groupByStatus([view({ status: "WEIRD" })]); + expect(lanes.OPEN).toHaveLength(1); + expect(groupByStatus(undefined).OPEN).toEqual([]); + }); +}); + +describe("isWindowApproaching", () => { + it("is true when current chapter is inside [from,to] for OPEN/PARTIAL", () => { + const f = view({ + status: "PARTIAL", + expected_close_from: 40, + expected_close_to: 60, + }); + expect(isWindowApproaching(f, 50)).toBe(true); + expect(isWindowApproaching(f, 39)).toBe(false); + expect(isWindowApproaching(f, 61)).toBe(false); + }); + + it("never flags CLOSED/OVERDUE or items without a window", () => { + expect( + isWindowApproaching( + view({ status: "CLOSED", expected_close_to: 60 }), + 50, + ), + ).toBe(false); + expect(isWindowApproaching(view({ status: "OPEN" }), 50)).toBe(false); + }); +}); + +describe("buildRegisterRequest", () => { + it("trims and omits empty optionals", () => { + expect( + buildRegisterRequest({ + projectId: "p1", + code: " F-007 ", + title: " 旧誓约 ", + content: " ", + expectedCloseTo: 110, + }), + ).toEqual({ code: "F-007", title: "旧誓约", expected_close_to: 110 }); + }); + + it("keeps numeric and non-empty string optionals", () => { + expect( + buildRegisterRequest({ + projectId: "p1", + code: "F-1", + title: "t", + plantedAt: 8, + content: "线索", + expectedCloseFrom: 40, + importance: "主线", + }), + ).toEqual({ + code: "F-1", + title: "t", + planted_at: 8, + content: "线索", + expected_close_from: 40, + importance: "主线", + }); + }); +}); + +describe("buildTransitionRequest", () => { + it("includes only provided fields", () => { + expect(buildTransitionRequest({ projectId: "p1", toStatus: "PARTIAL" })).toEqual({ + to_status: "PARTIAL", + }); + expect( + buildTransitionRequest({ + projectId: "p1", + progressEntry: { chapter: 23, note: "发光" }, + }), + ).toEqual({ progress_entry: { chapter: 23, note: "发光" } }); + expect(buildTransitionRequest({ projectId: "p1" })).toEqual({}); + expect( + buildTransitionRequest({ projectId: "p1", progressEntry: {} }), + ).toEqual({}); + }); +}); + +describe("validationReasonMessage / extractReason", () => { + it("maps known reasons to friendly text", () => { + expect(validationReasonMessage("duplicate")).toContain("已存在"); + expect(validationReasonMessage("invalid_transition")).toContain("终态"); + expect(validationReasonMessage("empty_update")).toContain("进展"); + expect(validationReasonMessage("invalid_status")).toContain("筛选"); + expect(validationReasonMessage(undefined)).toContain("校验"); + }); + + it("extracts details.reason from envelope", () => { + expect( + extractReason({ error: { details: { reason: "duplicate" } } }), + ).toBe("duplicate"); + expect(extractReason({ error: { details: null } })).toBeUndefined(); + expect(extractReason("oops")).toBeUndefined(); + }); +}); diff --git a/apps/web/lib/foreshadow/board.ts b/apps/web/lib/foreshadow/board.ts new file mode 100644 index 0000000..9bf3a5d --- /dev/null +++ b/apps/web/lib/foreshadow/board.ts @@ -0,0 +1,141 @@ +// 伏笔看板纯逻辑:四泳道分组、登记/转移请求体组装、422 reason 映射。 +// 对齐 C3 扩(GET/POST/PATCH .../foreshadow)。纯逻辑,便于 node 环境单测。 + +import type { + ForeshadowRegisterRequest, + ForeshadowTransitionRequest, + ForeshadowView, +} from "@/lib/api/types"; + +// 四状态泳道(UX §6.8);OVERDUE 用琥珀强调(渲染层处理)。 +export type ForeshadowStatus = "OPEN" | "PARTIAL" | "CLOSED" | "OVERDUE"; + +export const LANES: readonly ForeshadowStatus[] = [ + "OPEN", + "PARTIAL", + "CLOSED", + "OVERDUE", +] as const; + +export const LANE_LABELS: Record = { + OPEN: "OPEN", + PARTIAL: "PARTIAL", + CLOSED: "CLOSED", + OVERDUE: "OVERDUE", +}; + +export type ForeshadowLanes = Record; + +function emptyLanes(): ForeshadowLanes { + return { OPEN: [], PARTIAL: [], CLOSED: [], OVERDUE: [] }; +} + +function isStatus(v: string): v is ForeshadowStatus { + return v === "OPEN" || v === "PARTIAL" || v === "CLOSED" || v === "OVERDUE"; +} + +// 按 status 分四泳道(未知 status 落 OPEN 兜底);保持 repo 已排序的 code 顺序。 +export function groupByStatus( + items: readonly ForeshadowView[] | undefined, +): ForeshadowLanes { + const lanes = emptyLanes(); + for (const item of items ?? []) { + const status = isStatus(item.status) ? item.status : "OPEN"; + lanes[status] = [...lanes[status], item]; + } + return lanes; +} + +// 接近回收窗口判定:current 在 [from, to] 内,或已超 from 但尚未 CLOSED(提示安排回收)。 +// 仅对 OPEN/PARTIAL 提示;CLOSED 已回收、OVERDUE 已逾期另有强调。 +export function isWindowApproaching( + item: ForeshadowView, + currentChapter: number, +): boolean { + if (item.status === "CLOSED" || item.status === "OVERDUE") return false; + const to = item.expected_close_to; + if (typeof to !== "number") return false; + const from = item.expected_close_from ?? to; + return currentChapter >= from && currentChapter <= to; +} + +export interface RegisterInput { + projectId: string; + code: string; + title: string; + plantedAt?: number | null; + content?: string | null; + expectedCloseFrom?: number | null; + expectedCloseTo?: number | null; + importance?: string | null; +} + +// 组装登记请求体(去空白;空可选字段省略)。 +export function buildRegisterRequest( + input: RegisterInput, +): ForeshadowRegisterRequest { + const body: ForeshadowRegisterRequest = { + code: input.code.trim(), + title: input.title.trim(), + }; + if (typeof input.plantedAt === "number") body.planted_at = input.plantedAt; + const content = input.content?.trim(); + if (content) body.content = content; + if (typeof input.expectedCloseFrom === "number") + body.expected_close_from = input.expectedCloseFrom; + if (typeof input.expectedCloseTo === "number") + body.expected_close_to = input.expectedCloseTo; + const importance = input.importance?.trim(); + if (importance) body.importance = importance; + return body; +} + +export interface TransitionInput { + projectId: string; + toStatus?: ForeshadowStatus | null; + // 一条进展记录(自由 dict;如 {chapter, note})。 + progressEntry?: Record | null; +} + +// 组装转移/进展请求体(两字段皆可选;至少给一项由调用方校验)。 +export function buildTransitionRequest( + input: TransitionInput, +): ForeshadowTransitionRequest { + const body: ForeshadowTransitionRequest = {}; + if (input.toStatus) body.to_status = input.toStatus; + if (input.progressEntry && Object.keys(input.progressEntry).length > 0) + body.progress_entry = input.progressEntry; + return body; +} + +// 422 VALIDATION 信封 details.reason(C3 扩)→ 用户文案。 +export type ValidationReason = + | "duplicate" + | "invalid_transition" + | "empty_update" + | "invalid_status"; + +export function validationReasonMessage(reason: string | undefined): string { + switch (reason) { + case "duplicate": + return "该伏笔代号已存在,请换一个。"; + case "invalid_transition": + return "非法状态转移(CLOSED 为终态,不可再改)。"; + case "empty_update": + return "请填写目标状态或一条进展。"; + case "invalid_status": + return "无效的筛选状态。"; + default: + return "操作未通过校验,请检查输入。"; + } +} + +// 从错误信封安全提取 details.reason。 +export function extractReason(error: unknown): string | undefined { + if (typeof error !== "object" || error === null) return undefined; + const env = error as { + error?: { details?: { reason?: unknown } | null }; + }; + const reason = env.error?.details?.reason; + return typeof reason === "string" ? reason : undefined; +} diff --git a/apps/web/lib/foreshadow/useForeshadow.ts b/apps/web/lib/foreshadow/useForeshadow.ts new file mode 100644 index 0000000..a3013ed --- /dev/null +++ b/apps/web/lib/foreshadow/useForeshadow.ts @@ -0,0 +1,96 @@ +"use client"; + +import { useCallback, useState } from "react"; + +import { api } from "@/lib/api/client"; +import { useToast } from "@/components/Toast"; +import type { ForeshadowView } from "@/lib/api/types"; +import { + buildRegisterRequest, + buildTransitionRequest, + extractReason, + validationReasonMessage, + type ForeshadowStatus, + type RegisterInput, + type TransitionInput, +} from "./board"; + +export interface UseForeshadow { + items: ForeshadowView[]; + busy: boolean; + register: (input: RegisterInput) => Promise; + transition: (code: string, input: TransitionInput) => Promise; +} + +// 伏笔登记/状态变更(乐观更新 + 回滚 + Toast)。 +// register:成功后把返回行追加进 items;422 duplicate → 友好提示,不改 items。 +// transition:先乐观替换该行 status,失败回滚到旧 items 并 toast(读 details.reason)。 +export function useForeshadow(initial: ForeshadowView[]): UseForeshadow { + const [items, setItems] = useState(initial); + const [busy, setBusy] = useState(false); + const toast = useToast(); + + const register = useCallback( + async (input) => { + setBusy(true); + try { + const { data, error } = await api.POST( + "/projects/{project_id}/foreshadow", + { + params: { path: { project_id: input.projectId } }, + body: buildRegisterRequest(input), + }, + ); + if (error || !data) { + toast(validationReasonMessage(extractReason(error)), "error"); + return false; + } + setItems((prev) => [...prev, data]); + toast(`已登记伏笔 ${data.code}`, "success"); + return true; + } finally { + setBusy(false); + } + }, + [toast], + ); + + const transition = useCallback( + async (code, input) => { + const snapshot = items; + // 乐观:本地先改 status(若给了 toStatus)。 + if (input.toStatus) { + const next = input.toStatus as ForeshadowStatus; + setItems((prev) => + prev.map((it) => (it.code === code ? { ...it, status: next } : it)), + ); + } + setBusy(true); + try { + const { data, error } = await api.PATCH( + "/projects/{project_id}/foreshadow/{code}", + { + params: { + path: { project_id: input.projectId, code }, + }, + body: buildTransitionRequest(input), + }, + ); + if (error || !data) { + setItems(snapshot); // 回滚 + toast(validationReasonMessage(extractReason(error)), "error"); + return false; + } + // 用服务端权威行替换(status + progress 都对齐)。 + setItems((prev) => prev.map((it) => (it.code === code ? data : it))); + toast(`已更新 ${data.code}`, "success"); + return true; + } finally { + setBusy(false); + } + }, + [items, toast], + ); + + return { items, busy, register, transition }; +} diff --git a/apps/web/lib/outline/outline.test.ts b/apps/web/lib/outline/outline.test.ts new file mode 100644 index 0000000..716128b --- /dev/null +++ b/apps/web/lib/outline/outline.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; + +import type { + ForeshadowWindowView, + OutlineChapterView, +} from "@/lib/api/types"; +import { + groupByVolume, + isCloseWindow, + windowBadgeLabel, +} from "./outline"; + +const ch = (over: Partial): OutlineChapterView => ({ + no: 1, + volume: 1, + beats: [], + foreshadow_windows: [], + ...over, +}); + +const win = ( + over: Partial, +): ForeshadowWindowView => ({ code: "F-012", ...over }); + +describe("groupByVolume", () => { + it("groups by volume and sorts volumes and chapters ascending", () => { + const groups = groupByVolume([ + ch({ no: 127, volume: 3 }), + ch({ no: 12, volume: 1 }), + ch({ no: 126, volume: 3 }), + ]); + expect(groups.map((g) => g.volume)).toEqual([1, 3]); + expect(groups[1]?.chapters.map((c) => c.no)).toEqual([126, 127]); + }); + + it("handles undefined", () => { + expect(groupByVolume(undefined)).toEqual([]); + }); +}); + +describe("windowBadgeLabel", () => { + it("renders a range, half-open, or bare badge", () => { + expect( + windowBadgeLabel(win({ expected_close_from: 40, expected_close_to: 60 })), + ).toBe("⚑ F-012 (40-60)"); + expect(windowBadgeLabel(win({ expected_close_to: 60 }))).toBe( + "⚑ F-012 (≤60)", + ); + expect(windowBadgeLabel(win({ expected_close_from: 40 }))).toBe( + "⚑ F-012 (≥40)", + ); + expect(windowBadgeLabel(win({}))).toBe("⚑ F-012"); + }); +}); + +describe("isCloseWindow", () => { + it("is true when chapter no is inside the recovery window", () => { + const w = win({ expected_close_from: 40, expected_close_to: 60 }); + expect(isCloseWindow(ch({ no: 50 }), w)).toBe(true); + expect(isCloseWindow(ch({ no: 61 }), w)).toBe(false); + expect(isCloseWindow(ch({ no: 50 }), win({}))).toBe(false); + }); +}); diff --git a/apps/web/lib/outline/outline.ts b/apps/web/lib/outline/outline.ts new file mode 100644 index 0000000..776a8d6 --- /dev/null +++ b/apps/web/lib/outline/outline.ts @@ -0,0 +1,49 @@ +// 大纲编辑器纯逻辑:按卷分组、伏笔窗口徽标文案、接近回收窗口提示(UX §6.7)。 +// OutlineChapterView/ForeshadowWindowView 已由 OpenAPI 强类型,无需手动收窄。 + +import type { ForeshadowWindowView, OutlineChapterView } from "@/lib/api/types"; + +export interface VolumeGroup { + volume: number; + chapters: OutlineChapterView[]; +} + +// 按 volume 分组(保持章号升序);卷号升序。 +export function groupByVolume( + chapters: readonly OutlineChapterView[] | undefined, +): VolumeGroup[] { + const byVolume = new Map(); + for (const ch of chapters ?? []) { + const list = byVolume.get(ch.volume) ?? []; + byVolume.set(ch.volume, [...list, ch]); + } + return [...byVolume.entries()] + .sort(([a], [b]) => a - b) + .map(([volume, list]) => ({ + volume, + chapters: [...list].sort((a, b) => a.no - b.no), + })); +} + +// 伏笔窗口徽标文案:`⚑ F-012 (40-60)` / `⚑ F-012 (≤60)` / `⚑ F-012`。 +export function windowBadgeLabel(window: ForeshadowWindowView): string { + const from = window.expected_close_from; + const to = window.expected_close_to; + if (typeof from === "number" && typeof to === "number") { + return `⚑ ${window.code} (${from}-${to})`; + } + if (typeof to === "number") return `⚑ ${window.code} (≤${to})`; + if (typeof from === "number") return `⚑ ${window.code} (≥${from})`; + return `⚑ ${window.code}`; +} + +// 接近回收:本章号落在窗口 [from,to] 内(提示「本章可回收」,UX §6.7 ⚑ 可回收)。 +export function isCloseWindow( + chapter: OutlineChapterView, + window: ForeshadowWindowView, +): boolean { + const to = window.expected_close_to; + if (typeof to !== "number") return false; + const from = window.expected_close_from ?? to; + return chapter.no >= from && chapter.no <= to; +} diff --git a/apps/web/lib/outline/useOutline.ts b/apps/web/lib/outline/useOutline.ts new file mode 100644 index 0000000..3948052 --- /dev/null +++ b/apps/web/lib/outline/useOutline.ts @@ -0,0 +1,63 @@ +"use client"; + +import { useCallback, useState } from "react"; + +import { api } from "@/lib/api/client"; +import { useToast } from "@/components/Toast"; +import type { OutlineChapterView } from "@/lib/api/types"; + +export type OutlineStatus = "idle" | "generating" | "ready" | "error"; + +export interface OutlineError { + code: string; + message: string; +} + +export interface UseOutline { + chapters: OutlineChapterView[]; + status: OutlineStatus; + error: OutlineError | null; + generate: (projectId: string, volume: number) => Promise; +} + +// 生成大纲:POST .../outline(body {volume})。无凭据/网关失败 → 503 LLM_UNAVAILABLE +// (流前 JSON 信封),经 error 检出、引导去设置(同 draft/review 范式)。 +export function useOutline(initial: OutlineChapterView[]): UseOutline { + const [chapters, setChapters] = useState(initial); + const [status, setStatus] = useState( + initial.length > 0 ? "ready" : "idle", + ); + const [error, setError] = useState(null); + const toast = useToast(); + + const generate = useCallback( + async (projectId, volume) => { + setStatus("generating"); + setError(null); + const { data, error: apiError } = await api.POST( + "/projects/{project_id}/outline", + { + params: { path: { project_id: projectId } }, + body: { volume }, + }, + ); + if (apiError || !data) { + const env = apiError as + | { error?: { code?: string; message?: string } } + | undefined; + const code = env?.error?.code ?? "OUTLINE_FAILED"; + const message = env?.error?.message ?? "大纲生成失败,请重试。"; + setError({ code, message }); + setStatus("error"); + toast(message, "error"); + return; + } + setChapters(data.chapters ?? []); + setStatus("ready"); + toast("大纲已生成", "success"); + }, + [toast], + ); + + return { chapters, status, error, generate }; +} diff --git a/apps/web/lib/pace/beatmap.test.ts b/apps/web/lib/pace/beatmap.test.ts new file mode 100644 index 0000000..0756f7c --- /dev/null +++ b/apps/web/lib/pace/beatmap.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; + +import { SPARK_CHARS, toBeatCells, toSparkline } from "./beatmap"; + +describe("toBeatCells", () => { + it("maps min→▁ and max→▇ via linear normalization", () => { + const cells = toBeatCells([1, 7]); + expect(cells[0]?.level).toBe(0); + expect(cells[0]?.char).toBe("▁"); + expect(cells[1]?.level).toBe(SPARK_CHARS.length - 1); + expect(cells[1]?.char).toBe("▇"); + }); + + it("centers all cells when every value is equal", () => { + const cells = toBeatCells([5, 5, 5]); + expect(cells.every((c) => c.level === 3)).toBe(true); + expect(cells.every((c) => c.char === "▄")).toBe(true); + }); + + it("gives every cell a visible min height", () => { + const cells = toBeatCells([0, 10]); + expect(cells[0]?.heightPct).toBeGreaterThanOrEqual(12); + expect(cells[1]?.heightPct).toBe(100); + }); + + it("returns [] for empty or non-finite-only input", () => { + expect(toBeatCells([])).toEqual([]); + expect(toBeatCells([Number.NaN, Number.POSITIVE_INFINITY])).toEqual([]); + }); +}); + +describe("toSparkline", () => { + it("joins chars into a sparkline string", () => { + expect(toSparkline([1, 4, 7])).toBe("▁▄▇"); + expect(toSparkline([])).toBe(""); + }); +}); diff --git a/apps/web/lib/pace/beatmap.ts b/apps/web/lib/pace/beatmap.ts new file mode 100644 index 0000000..9d50b09 --- /dev/null +++ b/apps/web/lib/pace/beatmap.ts @@ -0,0 +1,53 @@ +// 爽点节拍图:把 pace.beat_map(int 序列)映射成 ▁▃▅ 风格的 sparkline 格子(UX §6.4)。 +// 纯逻辑,便于 node 环境单测;渲染时配合高度类与 prefers-reduced-motion。 + +// 七级火花字符(低→高),与高度档位一一对应。 +export const SPARK_CHARS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇"] as const; + +export interface BeatCell { + // 原始强度值。 + value: number; + // 归一化档位 0..6(映射到 SPARK_CHARS / 高度百分比)。 + level: number; + // 对应火花字符(▁..▇)。 + char: string; + // 渲染用高度百分比(12%..100%,最低也可见)。 + heightPct: number; +} + +const MIN_HEIGHT_PCT = 12; +const MAX_LEVEL = SPARK_CHARS.length - 1; + +// 把 beat_map 映射成可渲染的格子序列。 +// 归一化按「本序列 min..max」线性分桶到 0..6;全相等 → 全部居中档(level 3)。 +export function toBeatCells(beatMap: readonly number[]): BeatCell[] { + const values = beatMap.filter((v) => typeof v === "number" && Number.isFinite(v)); + if (values.length === 0) return []; + + const min = Math.min(...values); + const max = Math.max(...values); + const span = max - min; + + return values.map((value) => { + const level = + span === 0 + ? Math.round(MAX_LEVEL / 2) + : Math.round(((value - min) / span) * MAX_LEVEL); + const clamped = Math.min(MAX_LEVEL, Math.max(0, level)); + const heightPct = + MIN_HEIGHT_PCT + (clamped / MAX_LEVEL) * (100 - MIN_HEIGHT_PCT); + return { + value, + level: clamped, + char: SPARK_CHARS[clamped] ?? SPARK_CHARS[0], + heightPct: Math.round(heightPct), + }; + }); +} + +// 文本版 sparkline(无障碍/紧凑场景;UX §6.4 示例 ▁▃▅▂▁▇)。 +export function toSparkline(beatMap: readonly number[]): string { + return toBeatCells(beatMap) + .map((c) => c.char) + .join(""); +} diff --git a/apps/web/lib/review/history.test.ts b/apps/web/lib/review/history.test.ts index dd0feec..2d8da35 100644 --- a/apps/web/lib/review/history.test.ts +++ b/apps/web/lib/review/history.test.ts @@ -1,7 +1,12 @@ import { describe, expect, it } from "vitest"; import type { ReviewHistoryItem } from "@/lib/api/types"; -import { latestReview, normalizeConflicts } from "./history"; +import { + latestReview, + normalizeConflicts, + normalizeForeshadowSug, + normalizePace, +} from "./history"; const item = ( conflicts: Record[], @@ -43,6 +48,56 @@ describe("normalizeConflicts", () => { }); }); +describe("normalizeForeshadowSug", () => { + it("tightens flat list with kind, defaulting kind to planted", () => { + const out = normalizeForeshadowSug({ + ...item([]), + foreshadow_sug: [ + { kind: "resolved", code: "F-012", title: "神秘玉佩", where: "第4段" }, + { title: "残破地图" }, + ], + }); + expect(out).toEqual([ + { + kind: "resolved", + code: "F-012", + title: "神秘玉佩", + where: "第4段", + note: null, + }, + { kind: "planted", code: null, title: "残破地图", where: null, note: null }, + ]); + }); + + it("returns [] when missing", () => { + expect(normalizeForeshadowSug(item([]))).toEqual([]); + expect(normalizeForeshadowSug(undefined)).toEqual([]); + }); +}); + +describe("normalizePace", () => { + it("tightens dict into PaceReport, filtering bad water/beats", () => { + const out = normalizePace({ + ...item([]), + pace: { + water: [{ where: "第3段", reason: "注水" }, "junk"], + hook: true, + beat_map: [1, 3, "x", 5], + }, + }); + expect(out).toEqual({ + water: [{ where: "第3段", reason: "注水" }], + hook: true, + beat_map: [1, 3, 5], + }); + }); + + it("returns null when pace is missing or not a dict", () => { + expect(normalizePace(item([]))).toBeNull(); + expect(normalizePace(undefined)).toBeNull(); + }); +}); + describe("latestReview", () => { it("returns first item (history is newest-first)", () => { const a = item([]); diff --git a/apps/web/lib/review/history.ts b/apps/web/lib/review/history.ts index a50c5f9..63adf88 100644 --- a/apps/web/lib/review/history.ts +++ b/apps/web/lib/review/history.ts @@ -2,17 +2,32 @@ // 纯逻辑(可单测);schema 把 conflicts 标成 {[k]:unknown}[],这里安全收窄。 import type { ReviewHistoryItem } from "@/lib/api/types"; -import type { ReviewConflict } from "./sse"; +import type { + ForeshadowKind, + ForeshadowSuggestion, + PaceIssue, + PaceReport, + ReviewConflict, +} from "./sse"; function asString(v: unknown, fallback = ""): string { return typeof v === "string" ? v : fallback; } +function asOptionalString(v: unknown): string | null { + return typeof v === "string" ? v : null; +} + function asStringArray(v: unknown): string[] { if (!Array.isArray(v)) return []; return v.filter((x): x is string => typeof x === "string"); } +function asIntArray(v: unknown): number[] { + if (!Array.isArray(v)) return []; + return v.filter((x): x is number => typeof x === "number" && Number.isFinite(x)); +} + // 把一条留痕的 conflicts 收紧(缺字段给安全默认;保持顺序=下标身份,对齐冲突 gate)。 export function normalizeConflicts( item: ReviewHistoryItem | undefined, @@ -26,6 +41,46 @@ export function normalizeConflicts( })); } +// 把一条留痕的 foreshadow_sug(扁平 list,每条带 kind)收紧成建议(缺字段给安全默认)。 +// 对齐 collect 列映射(C4 扩 T3.3):planted/resolved 扁平成单 list、每条加 kind。 +export function normalizeForeshadowSug( + item: ReviewHistoryItem | undefined, +): ForeshadowSuggestion[] { + const raw = item?.foreshadow_sug ?? []; + return raw.map((s) => ({ + kind: asKind(s["kind"]), + code: asOptionalString(s["code"]), + title: asString(s["title"], "(未命名线索)"), + where: asOptionalString(s["where"]), + note: asOptionalString(s["note"]), + })); +} + +function asKind(v: unknown): ForeshadowKind { + return v === "resolved" ? "resolved" : "planted"; +} + +// 把一条留痕的 pace(dict)收紧成节奏报告;缺失/非 dict → null(不渲染节拍图)。 +export function normalizePace( + item: ReviewHistoryItem | undefined, +): PaceReport | null { + const raw = item?.pace; + if (typeof raw !== "object" || raw === null) return null; + const dict = raw as Record; + const waterRaw = Array.isArray(dict["water"]) ? dict["water"] : []; + const water: PaceIssue[] = waterRaw + .filter((w): w is Record => typeof w === "object" && w !== null) + .map((w) => ({ + where: asString(w["where"]), + reason: asString(w["reason"]), + })); + return { + water, + hook: dict["hook"] === true, + beat_map: asIntArray(dict["beat_map"]), + }; +} + // 最近一条留痕(GET .../reviews 已按新→旧排序)。 export function latestReview( items: readonly ReviewHistoryItem[] | undefined, diff --git a/apps/web/lib/review/sse.test.ts b/apps/web/lib/review/sse.test.ts index 7ff0390..81dc632 100644 --- a/apps/web/lib/review/sse.test.ts +++ b/apps/web/lib/review/sse.test.ts @@ -49,6 +49,35 @@ describe("parseReviewBlock", () => { }); }); + it("parses foreshadow and pace frames (C4 扩 T3.3)", () => { + expect( + parseReviewBlock( + 'event:foreshadow\ndata:{"kind":"resolved","code":"F-012","title":"神秘玉佩","where":"第4段","note":"疑似回收"}', + ), + ).toEqual({ + event: "foreshadow", + data: { + kind: "resolved", + code: "F-012", + title: "神秘玉佩", + where: "第4段", + note: "疑似回收", + }, + }); + expect( + parseReviewBlock( + 'event:pace\ndata:{"water":[{"where":"第3段","reason":"注水"}],"hook":false,"beat_map":[1,3,5,2,1,7]}', + ), + ).toEqual({ + event: "pace", + data: { + water: [{ where: "第3段", reason: "注水" }], + hook: false, + beat_map: [1, 3, 5, 2, 1, 7], + }, + }); + }); + it("returns null for unknown event or malformed json", () => { expect(parseReviewBlock('event:token\ndata:{"text":"x"}')).toBeNull(); expect(parseReviewBlock("event:conflict\ndata:{not json")).toBeNull(); @@ -107,6 +136,39 @@ describe("reduceReview", () => { ]); }); + it("accumulates foreshadow suggestions and replaces pace report (last wins)", () => { + const events: ReviewSseEvent[] = [ + { + event: "foreshadow", + data: { kind: "planted", title: "残破地图", where: "第5段" }, + }, + { + event: "foreshadow", + data: { kind: "resolved", code: "F-012", title: "神秘玉佩" }, + }, + { + event: "pace", + data: { water: [], hook: true, beat_map: [1, 2, 3] }, + }, + { + event: "pace", + data: { + water: [{ where: "第3段", reason: "注水" }], + hook: false, + beat_map: [3, 1, 5], + }, + }, + ]; + const state = events.reduce(reduceReview, initialReviewState); + expect(state.foreshadow.map((f) => f.kind)).toEqual([ + "planted", + "resolved", + ]); + expect(state.pace?.hook).toBe(false); + expect(state.pace?.beat_map).toEqual([3, 1, 5]); + expect(state.phase).toBe("reviewing"); + }); + it("marks done and captures error preserving collected data", () => { let s = reduceReview(initialReviewState, { event: "section", diff --git a/apps/web/lib/review/sse.ts b/apps/web/lib/review/sse.ts index ce903eb..c299e5c 100644 --- a/apps/web/lib/review/sse.ts +++ b/apps/web/lib/review/sse.ts @@ -19,6 +19,29 @@ export interface ReviewConflict { export type SectionStatus = "started" | "done" | "incomplete"; +// 伏笔建议(C4 扩 T3.3):planted=新埋待确认 / resolved=疑似回收(只读建议)。 +export type ForeshadowKind = "planted" | "resolved"; + +export interface ForeshadowSuggestion { + kind: ForeshadowKind; + code?: string | null; + title: string; + where?: string | null; + note?: string | null; +} + +// 节奏报告(C4 扩 T3.3):注水段 + 章末钩子 + 爽点节拍图。 +export interface PaceIssue { + where: string; + reason: string; +} + +export interface PaceReport { + water: PaceIssue[]; + hook: boolean; + beat_map: number[]; +} + export interface SectionEvent { event: "section"; data: { name: string; status: SectionStatus }; @@ -27,6 +50,24 @@ export interface ConflictEvent { event: "conflict"; data: ReviewConflict; } +export interface ForeshadowEvent { + event: "foreshadow"; + data: { + kind: ForeshadowKind; + code?: string | null; + title: string; + where?: string | null; + note?: string | null; + }; +} +export interface PaceEvent { + event: "pace"; + data: { + water: PaceIssue[]; + hook: boolean; + beat_map: number[]; + }; +} export interface DoneEvent { event: "done"; data: { length: number }; @@ -38,10 +79,19 @@ export interface ErrorEvent { export type ReviewSseEvent = | SectionEvent | ConflictEvent + | ForeshadowEvent + | PaceEvent | DoneEvent | ErrorEvent; -const KNOWN_EVENTS = new Set(["section", "conflict", "done", "error"]); +const KNOWN_EVENTS = new Set([ + "section", + "conflict", + "foreshadow", + "pace", + "done", + "error", +]); // 把一个完整 SSE 块(多行)解析成事件;无法解析则返回 null(跳过)。 export function parseReviewBlock(block: string): ReviewSseEvent | null { @@ -108,6 +158,8 @@ export interface ReviewStreamState { phase: ReviewPhase; sections: ReviewSection[]; conflicts: ReviewConflict[]; + foreshadow: ForeshadowSuggestion[]; + pace: PaceReport | null; error: { code: string; message: string; request_id?: string | null } | null; } @@ -115,6 +167,8 @@ export const initialReviewState: ReviewStreamState = { phase: "idle", sections: [], conflicts: [], + foreshadow: [], + pace: null, error: null, }; @@ -136,6 +190,18 @@ export function reduceReview( phase: "reviewing", conflicts: [...state.conflicts, event.data], }; + case "foreshadow": + return { + ...state, + phase: "reviewing", + foreshadow: [...state.foreshadow, { ...event.data }], + }; + case "pace": + return { + ...state, + phase: "reviewing", + pace: { ...event.data }, + }; case "done": return { ...state, phase: "done" }; case "error": diff --git a/apps/web/lib/review/useReviewStream.ts b/apps/web/lib/review/useReviewStream.ts index e9d8be9..887ffee 100644 --- a/apps/web/lib/review/useReviewStream.ts +++ b/apps/web/lib/review/useReviewStream.ts @@ -7,16 +7,24 @@ import { ReviewFrameBuffer, initialReviewState, reduceReview, + type ForeshadowSuggestion, + type PaceReport, type ReviewConflict, type ReviewStreamState, } from "./sse"; +export interface ReviewSeed { + conflicts: ReviewConflict[]; + foreshadow: ForeshadowSuggestion[]; + pace: PaceReport | null; +} + type Action = | { type: "start" } | { type: "events"; events: ReturnType } | { type: "abort" } | { type: "fail"; code: string; message: string } - | { type: "seed"; conflicts: ReviewConflict[] }; + | { type: "seed"; seed: ReviewSeed }; function reducer(state: ReviewStreamState, action: Action): ReviewStreamState { switch (action.type) { @@ -33,7 +41,12 @@ function reducer(state: ReviewStreamState, action: Action): ReviewStreamState { error: { code: action.code, message: action.message }, }; case "seed": - return { ...initialReviewState, conflicts: action.conflicts }; + return { + ...initialReviewState, + conflicts: action.seed.conflicts, + foreshadow: action.seed.foreshadow, + pace: action.seed.pace, + }; default: return state; } @@ -45,8 +58,8 @@ export interface UseReviewStream { // 用当前编辑器草稿重新审稿。 start: (projectId: string, chapterNo: number, draft: string) => Promise; stop: () => void; - // 进页用历史留痕的冲突种入(无需重审即可裁决)。 - seed: (conflicts: ReviewConflict[]) => void; + // 进页用历史留痕(冲突 + 伏笔建议 + 节奏)种入(无需重审即可裁决/查看)。 + seed: (seed: ReviewSeed) => void; } // 消费 POST .../review 的 SSE 流:fetch+ReadableStream(EventSource 不支持 POST)。 @@ -62,8 +75,8 @@ export function useReviewStream(): UseReviewStream { dispatch({ type: "abort" }); }, []); - const seed = useCallback((conflicts: ReviewConflict[]) => { - dispatch({ type: "seed", conflicts }); + const seed = useCallback((seedData: ReviewSeed) => { + dispatch({ type: "seed", seed: seedData }); }, []); const start = useCallback( diff --git a/apps/web/tsconfig.tsbuildinfo b/apps/web/tsconfig.tsbuildinfo index 640bf8a..c32d512 100644 --- a/apps/web/tsconfig.tsbuildinfo +++ b/apps/web/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"fileNames":["./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/.pnpm/@types+react@19.0.0/node_modules/@types/react/global.d.ts","./node_modules/.pnpm/csstype@3.2.3/node_modules/csstype/index.d.ts","./node_modules/.pnpm/@types+react@19.0.0/node_modules/@types/react/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/styled-jsx/types/css.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/styled-jsx/types/macro.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/styled-jsx/types/style.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/styled-jsx/types/global.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/styled-jsx/types/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/amp.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/amp.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/get-page-files.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/compatibility/disposable.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/compatibility/indexable.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/compatibility/index.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/globals.typedarray.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/buffer.buffer.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/globals.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/web-globals/events.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/header.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/readable.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/file.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/fetch.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/formdata.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/connector.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/client.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/errors.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/dispatcher.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-dispatcher.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-origin.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool-stats.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/handlers.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/balanced-pool.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/agent.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-interceptor.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-agent.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-client.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-pool.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-errors.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/proxy-agent.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-handler.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-agent.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/api.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/interceptors.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/util.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cookies.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/patch.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/websocket.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/eventsource.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/filereader.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/content-type.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cache.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/index.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/web-globals/storage.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/assert.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/assert/strict.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/async_hooks.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/buffer.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/child_process.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/cluster.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/console.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/constants.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/crypto.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/dgram.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/dns.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/dns/promises.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/domain.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/events.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/fs.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/fs/promises.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/http.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/http2.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/https.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/inspector.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/inspector.generated.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/module.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/net.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/os.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/path.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/perf_hooks.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/process.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/punycode.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/querystring.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/readline.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/readline/promises.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/repl.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/sea.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/sqlite.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/stream.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/stream/promises.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/stream/consumers.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/stream/web.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/string_decoder.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/test.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/timers.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/timers/promises.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/tls.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/trace_events.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/tty.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/url.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/util.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/v8.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/vm.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/wasi.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/worker_threads.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/zlib.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/index.d.ts","./node_modules/.pnpm/@types+react@19.0.0/node_modules/@types/react/canary.d.ts","./node_modules/.pnpm/@types+react@19.0.0/node_modules/@types/react/experimental.d.ts","./node_modules/.pnpm/@types+react-dom@19.0.0/node_modules/@types/react-dom/index.d.ts","./node_modules/.pnpm/@types+react-dom@19.0.0/node_modules/@types/react-dom/canary.d.ts","./node_modules/.pnpm/@types+react-dom@19.0.0/node_modules/@types/react-dom/experimental.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/fallback.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/webpack.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/config.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/load-custom-routes.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/image-config.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/body-streams.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/revalidate.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/setup-exception-listeners.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/worker.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/constants.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/app-router-headers.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/rendering-mode.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/require-hook.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/experimental/ppr.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/plugins/app-build-manifest-plugin.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/page-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/segment-config/app/app-segment-config.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/segment-config/pages/pages-segment-config.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/analysis/get-page-static-info.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-kind.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-definitions/route-definition.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-definitions/app-page-route-definition.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/cache-handlers/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/response-cache/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/resume-data-cache/cache-store.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/resume-data-cache/resume-data-cache.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/render-result.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/route-module.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/deep-readonly.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/load-components.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/flight-data-helpers.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-definitions/locale-route-definition.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-definitions/pages-route-definition.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/mitt.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/with-router.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/router.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/route-loader.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/page-loader.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/bloom-filter.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router/router.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/amp-context.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/pages/module.compiled.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/templates/pages.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/pages/module.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/render.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/response-cache/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-definitions/pages-api-route-definition.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-matches/pages-api-route-match.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/instrumentation/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-matchers/route-matcher.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/i18n-provider.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/normalizers/normalizer.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/normalizers/locale-route-normalizer.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/normalizers/request/pathname-normalizer.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/normalizers/request/suffix.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/normalizers/request/rsc.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/normalizers/request/prefetch-rsc.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/normalizers/request/next-data.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/after/builtin-request-context.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/base-server.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/next-url.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/cookies.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/request.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/response.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/segment-config/middleware/middleware-config.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/adapter.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/use-cache/cache-life.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/constants.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/page-extensions-type.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/loaders/next-app-loader/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/app-dir-module.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/cache-signal.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/work-unit-async-storage-instance.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/work-unit-async-storage.external.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router/utils/parse-relative-url.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request/fallback-params.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/clean-async-snapshot-instance.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/clean-async-snapshot.external.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/app-render.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/app-page/module.compiled.d.ts","./node_modules/.pnpm/@types+react@19.0.0/node_modules/@types/react/jsx-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/error-boundary.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/layout-router.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/render-from-template-context.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/action-async-storage-instance.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/action-async-storage.external.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/client-page.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/client-segment.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request/search-params.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/hooks-server-context.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/http-access-fallback/error-boundary.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/extra-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/resolvers.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/icons.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/resolve-metadata.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/metadata.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/metadata-boundary.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/rsc/preloads.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/rsc/postpone.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/rsc/taint.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/collect-segment-data.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/entry-base.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/templates/app-page.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/app-page/module.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-polyfill-crypto.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-environment-baseline.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-environment-extensions/error-inspect.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-environment-extensions/random.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-environment-extensions/date.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-environment-extensions/web-crypto.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-environment-extensions/node-crypto.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-environment.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-definitions/app-route-route-definition.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/async-storage/work-store.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/http.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/app-route/shared-modules.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/redirect-status-code.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/redirect-error.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/templates/app-route.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/app-route/module.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/app-route/module.compiled.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/segment-config/app/app-segments.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/utils.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/turborepo-access-trace/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/turborepo-access-trace/result.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/turborepo-access-trace/helpers.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/turborepo-access-trace/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/export/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/export/worker.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/worker.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/incremental-cache/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/after/after.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/after/after-context.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/work-async-storage-instance.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/work-async-storage.external.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request/params.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-matches/route-match.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request-meta.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/cli/next-test.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/config-shared.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/base-http/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/api-utils/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/base-http/node.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/async-callback-set.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","./node_modules/.pnpm/sharp@0.33.5/node_modules/sharp/lib/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/image-optimizer.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/next-server.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/coalesced-function.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/router-utils/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/trace/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/trace/trace.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/trace/shared.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/trace/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/load-jsconfig.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack-config.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/swc/generated-native.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/swc/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/dev/parse-version-info.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/react-dev-overlay/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/dev/hot-reloader-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/telemetry/storage.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/render-server.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/router-server.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/lru-cache.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/dev-bundler-service.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/dev/static-paths-worker.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/dev/next-dev-server.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/next.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","./node_modules/.pnpm/@next+env@15.1.3/node_modules/@next/env/dist/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/utils.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/pages/_app.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/app.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/use-cache/cache-tag.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/cache.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/runtime-config.external.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/config.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/pages/_document.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/document.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/dynamic.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dynamic.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/pages/_error.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/error.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/head.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/head.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request/cookies.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request/headers.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request/draft-mode.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/headers.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/get-img-props.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/image-component.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/image-external.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/image.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/link.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/link.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/redirect.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/not-found.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/forbidden.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/unauthorized.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/unstable-rethrow.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/navigation.react-server.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/navigation.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/navigation.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/router.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/script.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/script.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/image-response.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/@vercel/og/emoji/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/@vercel/og/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/after/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request/connection.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/server.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/types/global.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/types/compiled.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/image-types/global.d.ts","./next-env.d.ts","./node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/source-map.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/previous-map.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/input.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/declaration.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/root.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/warning.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/lazy-result.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/no-work-result.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/processor.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/result.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/document.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/rule.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/node.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/comment.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/container.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/at-rule.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/list.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/postcss.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/postcss.d.mts","./node_modules/.pnpm/tailwindcss@3.4.19/node_modules/tailwindcss/types/generated/corepluginlist.d.ts","./node_modules/.pnpm/tailwindcss@3.4.19/node_modules/tailwindcss/types/generated/colors.d.ts","./node_modules/.pnpm/tailwindcss@3.4.19/node_modules/tailwindcss/types/config.d.ts","./node_modules/.pnpm/tailwindcss@3.4.19/node_modules/tailwindcss/types/index.d.ts","./tailwind.config.ts","./node_modules/.pnpm/@types+estree@1.0.9/node_modules/@types/estree/index.d.ts","./node_modules/.pnpm/rollup@4.62.0/node_modules/rollup/dist/rollup.d.ts","./node_modules/.pnpm/rollup@4.62.0/node_modules/rollup/dist/parseast.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/types/hmrpayload.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/types/customevent.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/types/hot.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/dist/node/types.d-agj9qkwt.d.ts","./node_modules/.pnpm/esbuild@0.21.5/node_modules/esbuild/lib/main.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/dist/node/runtime.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/types/importglob.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/types/metadata.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/dist/node/index.d.ts","./node_modules/.pnpm/@vitest+pretty-format@2.1.9/node_modules/@vitest/pretty-format/dist/index.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/types.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/helpers.d.ts","./node_modules/.pnpm/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/index-c1cfc5e9.d.ts","./node_modules/.pnpm/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/node.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/index.d.ts","./node_modules/.pnpm/@vitest+runner@2.1.9/node_modules/@vitest/runner/dist/tasks-3znpj1lr.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/types-bxe-2udy.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/diff.d.ts","./node_modules/.pnpm/@vitest+runner@2.1.9/node_modules/@vitest/runner/dist/types.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/error.d.ts","./node_modules/.pnpm/@vitest+runner@2.1.9/node_modules/@vitest/runner/dist/index.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/environment.looobwuu.d.ts","./node_modules/.pnpm/@vitest+snapshot@2.1.9/node_modules/@vitest/snapshot/dist/environment-ddx0edty.d.ts","./node_modules/.pnpm/@vitest+snapshot@2.1.9/node_modules/@vitest/snapshot/dist/rawsnapshot-cpnkto81.d.ts","./node_modules/.pnpm/@vitest+snapshot@2.1.9/node_modules/@vitest/snapshot/dist/index.d.ts","./node_modules/.pnpm/@vitest+snapshot@2.1.9/node_modules/@vitest/snapshot/dist/environment.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/config.cy0c388z.d.ts","./node_modules/.pnpm/vite-node@2.1.9_@types+node@22.19.21/node_modules/vite-node/dist/trace-mapping.d-dlvdeqop.d.ts","./node_modules/.pnpm/vite-node@2.1.9_@types+node@22.19.21/node_modules/vite-node/dist/index-z0r8hvru.d.ts","./node_modules/.pnpm/vite-node@2.1.9_@types+node@22.19.21/node_modules/vite-node/dist/index.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/source-map.d.ts","./node_modules/.pnpm/vite-node@2.1.9_@types+node@22.19.21/node_modules/vite-node/dist/client.d.ts","./node_modules/.pnpm/vite-node@2.1.9_@types+node@22.19.21/node_modules/vite-node/dist/server.d.ts","./node_modules/.pnpm/@vitest+runner@2.1.9/node_modules/@vitest/runner/dist/utils.d.ts","./node_modules/.pnpm/tinybench@2.9.0/node_modules/tinybench/dist/index.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/benchmark.geerunq4.d.ts","./node_modules/.pnpm/@vitest+snapshot@2.1.9/node_modules/@vitest/snapshot/dist/manager.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/reporters.nr4dxcka.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/vite.czkp4x9w.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/config.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/config.d.ts","./vitest.config.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/worker.tn5kgiih.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/worker.b9fxpcac.d.ts","./node_modules/.pnpm/@vitest+expect@2.1.9/node_modules/@vitest/expect/dist/chai.d.cts","./node_modules/.pnpm/@vitest+expect@2.1.9/node_modules/@vitest/expect/dist/index.d.ts","./node_modules/.pnpm/@vitest+expect@2.1.9/node_modules/@vitest/expect/index.d.ts","./node_modules/.pnpm/@vitest+spy@2.1.9/node_modules/@vitest/spy/dist/index.d.ts","./node_modules/.pnpm/@vitest+mocker@2.1.9_vite@5.4.21_@types+node@22.19.21_/node_modules/@vitest/mocker/dist/types-dzoqtgin.d.ts","./node_modules/.pnpm/@vitest+mocker@2.1.9_vite@5.4.21_@types+node@22.19.21_/node_modules/@vitest/mocker/dist/index.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/mocker.crtm890j.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/suite.b2jumifp.d.ts","./node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/utils.d.ts","./node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/overloads.d.ts","./node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/branding.d.ts","./node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/messages.d.ts","./node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/index.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/index.d.ts","./lib/api/client.test.ts","./node_modules/.pnpm/openapi-typescript-helpers@0.0.15/node_modules/openapi-typescript-helpers/index.d.ts","./node_modules/.pnpm/openapi-fetch@0.13.8/node_modules/openapi-fetch/dist/index.d.ts","./lib/api/schema.d.ts","./lib/api/config.ts","./lib/api/client.ts","./lib/api/types.ts","./lib/api/server.ts","./lib/autosave/autosave.ts","./lib/autosave/autosave.test.ts","./components/toast.tsx","./lib/autosave/useautosave.ts","./lib/review/decisions.ts","./lib/review/decisions.test.ts","./lib/review/sse.ts","./lib/review/history.ts","./lib/review/history.test.ts","./lib/review/sse.test.ts","./lib/review/useaccept.ts","./lib/review/usereviewstream.ts","./lib/settings/providers.ts","./lib/stream/sse.ts","./lib/stream/sse.test.ts","./lib/stream/usedraftstream.ts","./lib/wizard/wizard.ts","./lib/wizard/wizard.test.ts","./app/layout.tsx","./components/leftnav.tsx","./components/appshell.tsx","./components/projectcard.tsx","./app/page.tsx","./components/review/acceptpanel.tsx","./components/review/annotatedtext.tsx","./components/review/conflictcard.tsx","./components/review/reviewreport.tsx","./app/projects/[id]/review/page.tsx","./components/workbench/chapterlist.tsx","./components/workbench/chapterassistant.tsx","./components/workbench/editor.tsx","./components/workbench/workbench.tsx","./app/projects/[id]/write/page.tsx","./components/projectwizard.tsx","./app/projects/new/page.tsx","./components/settings/providerssettings.tsx","./app/settings/providers/page.tsx","./.next/types/cache-life.d.ts","./.next/types/app/layout.ts","./.next/types/app/page.ts","./.next/types/app/projects/[id]/review/page.ts","./.next/types/app/projects/[id]/write/page.ts","./.next/types/app/projects/new/page.ts","./.next/types/app/settings/providers/page.ts"],"fileIdsList":[[76,124,141,142,308,562],[76,124,141,142,308,566],[76,124,141,142,308,571],[76,124,141,142,308,576],[76,124,141,142,308,578],[76,124,141,142,308,580],[76,124,141,142,399,400,401,402],[76,124,141,142,447,546],[76,124,141,142,423,542,543,564,565],[76,124,141,142,431,543,551,570],[76,124,141,142,431,543,575],[76,124,141,142,564,577],[76,124,141,142,542,543,564,579],[62,76,124,141,142,423,563],[76,124,141,142,423,431],[76,124,141,142,423,542],[62,76,124,141,142,431,541,546,560],[76,124,141,142,542],[76,124,141,142,550],[76,124,141,142,548,550],[62,76,124,141,142,542,548,550,551,554,555,564,567,568,569],[62,76,124,141,142,541,542,546,556],[62,76,124,141,142],[76,124,141,142],[62,76,124,141,142,423,542,547,559,564,572,573,574],[76,124,141,142,535],[76,124,141,142,538,539,540],[76,124,141,142,540,542],[76,124,141,142,539],[76,124,141,142,535,544],[62,76,124,141,142,541,544,546],[76,124,141,142,535,548],[76,124,141,142,535,542,551],[76,124,141,142,542,550],[76,124,141,142,535,550],[62,76,124,141,142,541,542,546,548],[62,76,124,141,142,540,550],[76,124,141,142,535,557],[62,76,124,141,142,540,557],[76,124,141,142,535,560],[76,124,141,142,447,448],[76,121,122,124,141,142],[76,123,124,141,142],[124,141,142],[76,124,129,141,142,159],[76,124,125,130,135,141,142,144,156,167],[76,124,125,126,135,141,142,144],[71,72,73,76,124,141,142],[76,124,127,141,142,168],[76,124,128,129,136,141,142,145],[76,124,129,141,142,156,164],[76,124,130,132,135,141,142,144],[76,123,124,131,141,142],[76,124,132,133,141,142],[76,124,134,135,141,142],[76,123,124,135,141,142],[76,124,135,136,137,141,142,156,167],[76,124,135,136,137,141,142,151,156,159],[76,117,124,132,135,138,141,142,144,156,167],[76,124,135,136,138,139,141,142,144,156,164,167],[76,124,138,140,141,142,156,164,167],[74,75,76,77,78,79,80,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173],[76,124,135,141,142],[76,124,141,142,143,167],[76,124,132,135,141,142,144,156],[76,124,141,142,145],[76,124,141,142,146],[76,123,124,141,142,147],[76,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173],[76,124,141,142,149],[76,124,141,142,150],[76,124,135,141,142,151,152],[76,124,141,142,151,153,168,170],[76,124,136,141,142],[76,124,135,141,142,156,157,159],[76,124,141,142,158,159],[76,124,141,142,156,157],[76,124,141,142,159],[76,124,141,142,160],[76,121,124,141,142,156,161,167],[76,124,135,141,142,162,163],[76,124,141,142,162,163],[76,124,129,141,142,144,156,164],[76,124,141,142,165],[76,124,141,142,144,166],[76,124,138,141,142,150,167],[76,124,129,141,142,168],[76,124,141,142,156,169],[76,124,141,142,143,170],[76,124,141,142,171],[76,117,124,141,142],[76,117,124,135,137,141,142,147,156,159,167,169,170,172],[76,124,141,142,156,173],[62,76,124,141,142,177,179],[62,76,124,141,142,177,178],[62,66,76,124,141,142,176,393,440],[62,66,76,124,141,142,175,393,440],[60,61,76,124,141,142],[76,124,141,142,491,492,495],[76,124,141,142,523],[76,124,141,142,526],[76,124,141,142,492,493,495,496,497],[76,124,141,142,492],[76,124,141,142,492,493,495],[76,124,141,142,492,493],[76,124,141,142,500],[76,124,141,142,487,500,501],[76,124,141,142,487,500],[76,124,141,142,487,494],[76,124,141,142,488],[76,124,141,142,487,488,489,491],[76,124,141,142,487],[76,124,141,142,530,531],[76,124,141,142,530,531,532,533],[76,124,141,142,530,532],[76,124,141,142,530],[68,76,124,141,142],[76,124,141,142,397],[76,124,141,142,404],[76,124,141,142,183,196,197,198,200,357],[76,124,141,142,183,187,189,190,191,192,346,357,359],[76,124,141,142,357],[76,124,141,142,197,213,290,337,353],[76,124,141,142,183],[76,124,141,142,377],[76,124,141,142,357,359,376],[76,124,141,142,276,290,318,445],[76,124,141,142,283,300,337,352],[76,124,141,142,238],[76,124,141,142,341],[76,124,141,142,340,341,342],[76,124,141,142,340],[70,76,124,138,141,142,180,183,190,193,194,195,197,201,269,274,320,328,338,348,357,393],[76,124,141,142,183,199,227,272,357,373,374,445],[76,124,141,142,199,445],[76,124,141,142,272,273,274,357,445],[76,124,141,142,445],[76,124,141,142,183,199,200,445],[76,124,141,142,193,339,345],[76,124,141,142,150,291,353],[76,124,141,142,291,353],[62,76,124,141,142,291],[62,76,124,141,142,270,291,292],[76,124,141,142,218,236,353,429],[76,124,141,142,334,424,425,426,427,428],[76,124,141,142,333],[76,124,141,142,333,334],[76,124,141,142,191,215,216,270],[76,124,141,142,217,218,270],[76,124,141,142,270],[62,76,124,141,142,184,418],[62,76,124,141,142,167],[62,76,124,141,142,199,225],[62,76,124,141,142,199],[76,124,141,142,223,228],[62,76,124,141,142,224,396],[62,66,76,124,138,141,142,174,175,176,393,438,439],[76,124,136,138,141,142,187,213,241,259,270,343,357,358,445],[76,124,141,142,328,344],[76,124,141,142,393],[76,124,141,142,182],[76,124,141,142,150,276,288,309,311,352,353],[76,124,141,142,150,276,288,308,309,310,352,353],[76,124,141,142,302,303,304,305,306,307],[76,124,141,142,304],[76,124,141,142,308],[62,76,124,141,142,224,291,396],[62,76,124,141,142,291,394,396],[62,76,124,141,142,291,396],[76,124,141,142,259,349],[76,124,141,142,349],[76,124,138,141,142,358,396],[76,124,141,142,296],[76,123,124,141,142,295],[76,124,141,142,209,210,212,242,270,283,284,285,287,320,352,355,358],[76,124,141,142,286],[76,124,141,142,210,218,270],[76,124,141,142,283,352],[76,124,141,142,283,292,293,294,296,297,298,299,300,301,312,313,314,315,316,317,352,353,445],[76,124,141,142,281],[76,124,138,141,142,150,187,208,210,212,213,214,218,246,259,268,269,320,348,357,358,359,393,445],[76,124,141,142,352],[76,123,124,141,142,197,212,269,285,300,348,350,351,358],[76,124,141,142,283],[76,123,124,141,142,208,242,262,277,278,279,280,281,282],[76,124,138,141,142,262,263,277,358,359],[76,124,141,142,197,259,269,270,285,348,352,358],[76,124,138,141,142,357,359],[76,124,138,141,142,156,355,358,359],[76,124,138,141,142,150,167,180,187,199,209,210,212,213,214,219,241,242,243,245,246,249,250,252,255,256,257,258,270,347,348,353,355,357,358,359],[76,124,138,141,142,156],[76,124,141,142,183,184,185,187,194,355,356,393,396,445],[76,124,138,141,142,156,167,203,375,377,378,379,445],[76,124,141,142,150,167,180,203,213,242,243,250,259,267,270,348,353,355,360,361,367,373,389,390],[76,124,141,142,193,194,269,328,339,348,357],[76,124,138,141,142,167,184,242,355,357,365],[76,124,141,142,275],[76,124,138,141,142,386,387,388],[76,124,141,142,355,357],[76,124,141,142,187,212,242,347,396],[76,124,138,141,142,150,250,259,355,361,367,369,373,389,392],[76,124,138,141,142,193,328,373,382],[76,124,141,142,183,219,347,357,384],[76,124,138,141,142,199,219,357,368,369,380,381,383,385],[70,76,124,141,142,210,211,212,393,396],[76,124,138,141,142,150,167,187,193,201,209,213,214,242,243,245,246,258,259,267,270,328,347,348,353,354,355,360,361,362,364,366,396],[76,124,138,141,142,156,193,355,367,386,391],[76,124,141,142,323,324,325,326,327],[76,124,141,142,249,251],[76,124,141,142,253],[76,124,141,142,251],[76,124,141,142,253,254],[76,124,138,141,142,187,208,358],[62,76,124,138,141,142,150,182,184,187,209,210,212,213,214,240,355,359,393,396],[76,124,138,141,142,150,167,186,191,242,354,358],[76,124,141,142,277],[76,124,141,142,278],[76,124,141,142,279],[76,124,141,142,202,206],[76,124,138,141,142,187,202,209],[76,124,141,142,205,206],[76,124,141,142,207],[76,124,141,142,202,203],[76,124,141,142,202,220],[76,124,141,142,202],[76,124,141,142,248,249,354],[76,124,141,142,247],[76,124,141,142,203,353,354],[76,124,141,142,244,354],[76,124,141,142,203,353],[76,124,141,142,320],[76,124,141,142,204,209,211,242,270,276,285,288,289,319,355,358],[76,124,141,142,218,229,232,233,234,235,236],[76,124,141,142,336],[76,124,141,142,197,211,212,263,270,283,296,300,329,330,331,332,334,335,338,347,352,357],[76,124,141,142,218],[76,124,141,142,240],[76,124,138,141,142,209,211,221,237,239,241,355,393,396],[76,124,141,142,218,229,230,231,232,233,234,235,236,394],[76,124,141,142,203],[76,124,141,142,263,264,267,348],[76,124,138,141,142,249,357],[76,124,138,141,142],[76,124,141,142,262,283],[76,124,141,142,261],[76,124,141,142,258,263],[76,124,141,142,260,262,357],[76,124,138,141,142,186,263,264,265,266,357,358],[62,76,124,141,142,215,217,270],[76,124,141,142,271],[62,76,124,141,142,184],[62,76,124,141,142,353],[62,70,76,124,141,142,212,214,393,396],[76,124,141,142,184,418,419],[62,76,124,141,142,228],[62,76,124,141,142,150,167,182,222,224,226,227,396],[76,124,141,142,199,353,358],[76,124,141,142,353,363],[62,76,124,136,138,141,142,150,182,228,272,393,394,395],[62,76,124,141,142,175,176,393,440],[62,63,64,65,66,76,124,141,142],[76,124,129,141,142],[76,124,141,142,370,371,372],[76,124,141,142,370],[62,66,76,124,138,140,141,142,150,174,175,176,177,179,180,182,246,308,359,392,396,440],[76,124,141,142,406],[76,124,141,142,408],[76,124,141,142,410],[76,124,141,142,412],[76,124,141,142,414,415,416],[76,124,141,142,420],[67,69,76,124,141,142,398,403,405,407,409,411,413,417,421,423,431,432,434,443,444,445,446],[76,124,141,142,422],[76,124,141,142,430],[76,124,141,142,224],[76,124,141,142,433],[76,123,124,141,142,263,264,265,267,299,353,435,436,437,440,441,442],[76,124,141,142,174],[76,124,141,142,537],[76,124,141,142,465],[76,124,141,142,463,465],[76,124,141,142,454,462,463,464,466,468],[76,124,141,142,452],[76,124,141,142,455,460,465,468],[76,124,141,142,451,468],[76,124,141,142,455,456,459,460,461,468],[76,124,141,142,455,456,457,459,460,468],[76,124,141,142,452,453,454,455,456,460,461,462,464,465,466,468],[76,124,141,142,468],[76,124,141,142,450,452,453,454,455,456,457,459,460,461,462,463,464,465,466,467],[76,124,141,142,450,468],[76,124,141,142,455,457,458,460,461,468],[76,124,141,142,459,468],[76,124,141,142,460,461,465,468],[76,124,141,142,453,463],[76,124,141,142,476,485],[76,124,141,142,475,476],[76,124,141,142,156,174],[76,124,141,142,470,471],[76,124,141,142,469,472],[76,124,141,142,490],[76,89,93,124,141,142,167],[76,89,124,141,142,156,167],[76,84,124,141,142],[76,86,89,124,141,142,164,167],[76,124,141,142,144,164],[76,84,124,141,142,174],[76,86,89,124,141,142,144,167],[76,81,82,85,88,124,135,141,142,156,167],[76,89,96,124,141,142],[76,81,87,124,141,142],[76,89,110,111,124,141,142],[76,85,89,124,141,142,159,167,174],[76,110,124,141,142,174],[76,83,84,124,141,142,174],[76,89,124,141,142],[76,83,84,85,86,87,88,89,90,91,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,111,112,113,114,115,116,124,141,142],[76,89,104,124,141,142],[76,89,96,97,124,141,142],[76,87,89,97,98,124,141,142],[76,88,124,141,142],[76,81,84,89,124,141,142],[76,89,93,97,98,124,141,142],[76,93,124,141,142],[76,87,89,92,124,141,142,167],[76,81,86,89,96,124,141,142],[76,124,141,142,156],[76,84,89,110,124,141,142,172,174],[76,124,141,142,505,506],[76,124,141,142,505],[76,124,141,142,486,505,506,516],[76,124,135,136,138,139,140,141,142,144,156,164,167,173,174,469,476,477,478,479,480,481,482,483,484,485],[76,124,141,142,478,479,480,481],[76,124,141,142,478,479,480],[76,124,141,142,478],[76,124,141,142,479],[76,124,141,142,476],[76,124,141,142,517],[76,124,141,142,498,511,512,535],[76,124,141,142,487,498,502,503,535],[76,124,141,142,527],[76,124,136,141,142,156,486,487,492,498,499,502,504,507,508,509,510,513,514,516,522,535],[76,124,141,142,498,511,512,513,535],[76,124,141,142,486,515],[76,124,141,142,172,520],[76,124,141,142,498,499,502,504,507,535],[76,124,136,141,142,156,486,487,492,498,499,502,503,504,507,508,509,510,511,512,513,514,515,516,522,535],[76,124,136,141,142,156,172,486,487,492,495,498,499,502,503,504,507,508,509,510,511,512,513,514,515,516,520,521,522,524,525,527,528,529,534,535],[76,124,141,142,473],[76,124,141,142,146,518]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"36a2e4c9a67439aca5f91bb304611d5ae6e20d420503e96c230cf8fcdc948d94","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"a305ee2f90e34e9e70aba9a9e9a154ce20c4d5cd1499cd21b8dc3617e1e5c810","impliedFormat":1},{"version":"acd8fd5090ac73902278889c38336ff3f48af6ba03aa665eb34a75e7ba1dccc4","impliedFormat":1},{"version":"d6258883868fb2680d2ca96bc8b1352cab69874581493e6d52680c5ffecdb6cc","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"f258e3960f324a956fc76a3d3d9e964fff2244ff5859dcc6ce5951e5413ca826","impliedFormat":1},{"version":"643f7232d07bf75e15bd8f658f664d6183a0efaca5eb84b48201c7671a266979","impliedFormat":1},{"version":"0f6666b58e9276ac3a38fdc80993d19208442d6027ab885580d93aec76b4ef00","impliedFormat":1},{"version":"05fd364b8ef02fb1e174fbac8b825bdb1e5a36a016997c8e421f5fab0a6da0a0","impliedFormat":1},{"version":"631eff75b0e35d1b1b31081d55209abc43e16b49426546ab5a9b40bdd40b1f60","impliedFormat":1},{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"808069bba06b6768b62fd22429b53362e7af342da4a236ed2d2e1c89fcca3b4a","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"b52476feb4a0cbcb25e5931b930fc73cb6643fb1a5060bf8a3dda0eeae5b4b68","affectsGlobalScope":true,"impliedFormat":1},{"version":"f9501cc13ce624c72b61f12b3963e84fad210fbdf0ffbc4590e08460a3f04eba","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0fa06ada475b910e2106c98c68b10483dc8811d0c14a8a8dd36efb2672485b29","impliedFormat":1},{"version":"33e5e9aba62c3193d10d1d33ae1fa75c46a1171cf76fef750777377d53b0303f","impliedFormat":1},{"version":"2b06b93fd01bcd49d1a6bd1f9b65ddcae6480b9a86e9061634d6f8e354c1468f","impliedFormat":1},{"version":"6a0cd27e5dc2cfbe039e731cf879d12b0e2dded06d1b1dedad07f7712de0d7f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"13f5c844119c43e51ce777c509267f14d6aaf31eafb2c2b002ca35584cd13b29","impliedFormat":1},{"version":"e60477649d6ad21542bd2dc7e3d9ff6853d0797ba9f689ba2f6653818999c264","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"4c829ab315f57c5442c6667b53769975acbf92003a66aef19bce151987675bd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"b2ade7657e2db96d18315694789eff2ddd3d8aea7215b181f8a0b303277cc579","impliedFormat":1},{"version":"9855e02d837744303391e5623a531734443a5f8e6e8755e018c41d63ad797db2","impliedFormat":1},{"version":"4d631b81fa2f07a0e63a9a143d6a82c25c5f051298651a9b69176ba28930756d","impliedFormat":1},{"version":"836a356aae992ff3c28a0212e3eabcb76dd4b0cc06bcb9607aeef560661b860d","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"41670ee38943d9cbb4924e436f56fc19ee94232bc96108562de1a734af20dc2c","affectsGlobalScope":true,"impliedFormat":1},{"version":"c906fb15bd2aabc9ed1e3f44eb6a8661199d6c320b3aa196b826121552cb3695","impliedFormat":1},{"version":"22295e8103f1d6d8ea4b5d6211e43421fe4564e34d0dd8e09e520e452d89e659","impliedFormat":1},{"version":"58647d85d0f722a1ce9de50955df60a7489f0593bf1a7015521efe901c06d770","impliedFormat":1},{"version":"9018e9493596e2d041fc531f5101a0d05f9d2e1311f4ae3c74345b1766158c08","impliedFormat":1},{"version":"a10f0e1854f3316d7ee437b79649e5a6ae3ae14ffe6322b02d4987071a95362e","impliedFormat":1},{"version":"e208f73ef6a980104304b0d2ca5f6bf1b85de6009d2c7e404028b875020fa8f2","impliedFormat":1},{"version":"d163b6bc2372b4f07260747cbc6c0a6405ab3fbcea3852305e98ac43ca59f5bc","impliedFormat":1},{"version":"e6fa9ad47c5f71ff733744a029d1dc472c618de53804eae08ffc243b936f87ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"a6f137d651076822d4fe884287e68fd61785a0d3d1fdb250a5059b691fa897db","impliedFormat":1},{"version":"24826ed94a78d5c64bd857570fdbd96229ad41b5cb654c08d75a9845e3ab7dde","impliedFormat":1},{"version":"8b479a130ccb62e98f11f136d3ac80f2984fdc07616516d29881f3061f2dd472","impliedFormat":1},{"version":"928af3d90454bf656a52a48679f199f64c1435247d6189d1caf4c68f2eaf921f","affectsGlobalScope":true,"impliedFormat":1},{"version":"bceb58df66ab8fb00170df20cd813978c5ab84be1d285710c4eb005d8e9d8efb","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"4f9d8ca0c417b67b69eeb54c7ca1bedd7b56034bb9bfd27c5d4f3bc4692daca7","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"a3fc63c0d7b031693f665f5494412ba4b551fe644ededccc0ab5922401079c95","impliedFormat":1},{"version":"80523c00b8544a2000ae0143e4a90a00b47f99823eb7926c1e03c494216fc363","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"746911b62b329587939560deb5c036aca48aece03147b021fa680223255d5183","affectsGlobalScope":true,"impliedFormat":1},{"version":"18fd40412d102c5564136f29735e5d1c3b455b8a37f920da79561f1fde068208","impliedFormat":1},{"version":"c8d3e5a18ba35629954e48c4cc8f11dc88224650067a172685c736b27a34a4dc","impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"2b55d426ff2b9087485e52ac4bc7cfafe1dc420fc76dad926cd46526567c501a","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"5b7aa3c4c1a5d81b411e8cb302b45507fea9358d3569196b27eb1a27ae3a90ef","affectsGlobalScope":true,"impliedFormat":1},{"version":"5987a903da92c7462e0b35704ce7da94d7fdc4b89a984871c0e2b87a8aae9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea08a0345023ade2b47fbff5a76d0d0ed8bff10bc9d22b83f40858a8e941501c","impliedFormat":1},{"version":"47613031a5a31510831304405af561b0ffaedb734437c595256bb61a90f9311b","impliedFormat":1},{"version":"ae062ce7d9510060c5d7e7952ae379224fb3f8f2dd74e88959878af2057c143b","impliedFormat":1},{"version":"8a1a0d0a4a06a8d278947fcb66bf684f117bf147f89b06e50662d79a53be3e9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"358765d5ea8afd285d4fd1532e78b88273f18cb3f87403a9b16fef61ac9fdcfe","impliedFormat":1},{"version":"9f55299850d4f0921e79b6bf344b47c420ce0f507b9dcf593e532b09ea7eeea1","impliedFormat":1},{"version":"865a2612f5ec073dd48d454307ccabb04c48f8b96fda9940c5ebfe6b4b451f51","impliedFormat":1},{"version":"89e7fd23f6e6ced38596054161f5fb88737018909c6529c946cb349b74b95275","impliedFormat":1},{"version":"115b2ad73fa7d175cd71a5873d984c21593b2a022f1a2036cc39d9f53629e5dc","impliedFormat":1},{"version":"1be330b3a0b00590633f04c3b35db7fa618c9ee079258e2b24c137eb4ffcd728","impliedFormat":1},{"version":"bb7a61dd55dc4b9422d13da3a6bb9cc5e89be888ef23bbcf6558aa9726b89a1c","impliedFormat":1},{"version":"3da0083607976261730c44908eab1b6262f727747ef3230a65ecd0153d9e8639","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"dd721e5707f241e4ef4ab36570d9e2a79f66aad63a339e3cbdbac7d9164d2431","impliedFormat":1},{"version":"24f8562308dd8ba6013120557fa7b44950b619610b2c6cb8784c79f11e3c4f90","impliedFormat":1},{"version":"3849a7f92d0e11b785f6ae7bedb25d9aad8d1234b3f1cf530a4e7404be26dd0a","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"57d6ac03382e30e9213641ff4f18cf9402bb246b77c13c8e848c0b1ca2b7ef92","impliedFormat":1},{"version":"f040772329d757ecd38479991101ef7bc9bf8d8f4dd8ee5d96fe00aa264f2a2b","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"57e47d02e88abef89d214cdf52b478104dc17997015746e288cbb580beaef266","impliedFormat":1},{"version":"04a2d0bd8166f057cc980608bd5898bfc91198636af3c1eb6cb4eb5e8652fbea","impliedFormat":1},{"version":"376c21ad92ca004531807ea4498f90a740fd04598b45a19335a865408180eddd","impliedFormat":1},{"version":"9e2739b32f741859263fdba0244c194ca8e96da49b430377930b8f721d77c000","impliedFormat":1},{"version":"a9af0e608929aaf9ce96bd7a7b99c9360636c31d73670e4af09a09950df97841","impliedFormat":1},{"version":"48d37b90a04e753a925228f50304d02c4f95d57bf682f8bb688621c3cd9d32ec","impliedFormat":1},{"version":"361e2b13c6765d7f85bb7600b48fde782b90c7c41105b7dab1f6e7871071ba20","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"b6db56e4903e9c32e533b78ac85522de734b3d3a8541bf24d256058d464bf04b","impliedFormat":1},{"version":"24daa0366f837d22c94a5c0bad5bf1fd0f6b29e1fae92dc47c3072c3fdb2fbd5","impliedFormat":1},{"version":"b68c4ed987ef5693d3dccd85222d60769463aca404f2ffca1c4c42781dce388e","impliedFormat":1},{"version":"cfb5b5d514eb4ad0ee25f313b197f3baa493eee31f27613facd71efb68206720","impliedFormat":1},{"version":"65f43099ded6073336e697512d9b80f2d4fec3182b7b2316abf712e84104db00","impliedFormat":1},{"version":"3e7efde639c6a6c3edb9847b3f61e308bf7a69685b92f665048c45132f51c218","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"9715fe982fccf375c88ac4d3cc8f6a126a7b7596be8d60190a0c7d22b45b4be4","impliedFormat":1},{"version":"1fe24e25a00c7dd689cb8c0fb4f1048b4a6d1c50f76aaca2ca5c6cdb44e01442","impliedFormat":1},{"version":"672f293c53a07b8c1c1940797cd5c7984482a0df3dd9c1f14aaee8d3474c2d83","impliedFormat":1},{"version":"0a66cb2511fa8e3e0e6ba9c09923f664a0a00896f486e6f09fc11ff806a12b0c","impliedFormat":1},{"version":"d703f98676a44f90d63b3ffc791faac42c2af0dd2b4a312f4afdb5db471df3de","impliedFormat":1},{"version":"0cfe1d0b90d24f5c105db5a2117192d082f7d048801d22a9ea5c62fae07b80a0","impliedFormat":1},{"version":"ef61792acbfa8c27c9bd113f02731e66229f7d3a169e3c1993b508134f1a58e0","impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","impliedFormat":1},{"version":"414cc05e215b7fc5a4a6ece431985e05e03762c8eb5bf1e0972d477f97832956","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"5c2e5ca7d53236bbf483a81ae283e2695e291fe69490cd139b33fa9e71838a69","impliedFormat":1},{"version":"a73bee51e3820392023252c36348e62dd72e6bae30a345166e9c78360f1aba7e","impliedFormat":1},{"version":"6ea68b3b7d342d1716cc4293813410d3f09ff1d1ca4be14c42e6d51e810962e1","impliedFormat":1},{"version":"c319e82ac16a5a5da9e28dfdefdad72cebb5e1e67cbdcc63cce8ae86be1e454f","impliedFormat":1},{"version":"a23185bc5ef590c287c28a91baf280367b50ae4ea40327366ad01f6f4a8edbc5","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"0c7c947ff881c4274c0800deaa0086971e0bfe51f89a33bd3048eaa3792d4876","affectsGlobalScope":true,"impliedFormat":1},{"version":"db01d18853469bcb5601b9fc9826931cc84cc1a1944b33cad76fd6f1e3d8c544","affectsGlobalScope":true,"impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"a020158a317c07774393974d26723af551e569f1ba4d6524e8e245f10e11b976","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"15b36126e0089bfef173ab61329e8286ce74af5e809d8a72edcafd0cc049057f","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"13283350547389802aa35d9f2188effaeac805499169a06ef5cd77ce2a0bd63f","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"d07cbc787a997d83f7bde3877fec5fb5b12ce8c1b7047eb792996ed9726b4dde","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"8bba776476c48b0e319d243f353190f24096057acede3c2f620fee17ff885dba","impliedFormat":1},{"version":"a3abe92070fbd33714bd837806030b39cfb1f8283a98c7c1f55fffeea388809e","impliedFormat":1},{"version":"ceb6696b98a72f2dae802260c5b0940ea338de65edd372ff9e13ab0a410c3a88","impliedFormat":1},{"version":"2cd914e04d403bdc7263074c63168335d44ce9367e8a74f6896c77d4d26a1038","impliedFormat":1},{"version":"ac60bbee0d4235643cc52b57768b22de8c257c12bd8c2039860540cab1fa1d82","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"208c9af9429dd3c76f5927b971263174aaa4bc7621ddec63f163640cbd3c473c","impliedFormat":1},{"version":"20865ac316b8893c1a0cc383ccfc1801443fbcc2a7255be166cf90d03fac88c9","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"3bc8605900fd1668f6d93ce8e14386478b6caa6fda41be633ee0fe4d0c716e62","impliedFormat":1},{"version":"461d0ad8ae5f2ff981778af912ba71b37a8426a33301daa00f21c6ccb27f8156","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"fcafff163ca5e66d3b87126e756e1b6dfa8c526aa9cd2a2b0a9da837d81bbd72","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"45490817629431853543adcb91c0673c25af52a456479588b6486daba34f68bb","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"b01bd582a6e41457bc56e6f0f9de4cb17f33f5f3843a7cf8210ac9c18472fb0f","impliedFormat":1},{"version":"9f31420a5040dbfb49ab94bcaaa5103a9a464e607cabe288958f53303f1da32e","impliedFormat":1},{"version":"6124e973eab8c52cabf3c07575204efc1784aca6b0a30c79eb85fe240a857efa","impliedFormat":1},{"version":"0d891735a21edc75df51f3eb995e18149e119d1ce22fd40db2b260c5960b914e","impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","impliedFormat":1},{"version":"f11d0dcaa4a1cba6d6513b04ceb31a262f223f56e18b289c0ba3133b4d3cd9a6","impliedFormat":1},{"version":"0a437ae178f999b46b6153d79095b60c42c996bc0458c04955f1c996dc68b971","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"4a7baeb6325920044f66c0f8e5e6f1f52e06e6d87588d837bdf44feb6f35c664","impliedFormat":1},{"version":"6dcf60530c25194a9ee0962230e874ff29d34c59605d8e069a49928759a17e0a","impliedFormat":1},{"version":"56013416784a6b754f3855f8f2bf6ce132320679b8a435389aca0361bce4df6b","impliedFormat":1},{"version":"43e96a3d5d1411ab40ba2f61d6a3192e58177bcf3b133a80ad2a16591611726d","impliedFormat":1},{"version":"9c066f3b46cf016e5d072b464821c5b21cc9adcc44743de0f6c75e2509a357ab","impliedFormat":1},{"version":"002eae065e6960458bda3cf695e578b0d1e2785523476f8a9170b103c709cd4f","impliedFormat":1},{"version":"c51641ab4bfa31b7a50a0ca37edff67f56fab3149881024345b13f2b48b7d2de","impliedFormat":1},{"version":"a57b1802794433adec9ff3fed12aa79d671faed86c49b09e02e1ac41b4f1d33a","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"52abbd5035a97ebfb4240ec8ade2741229a7c26450c84eb73490dc5ea048b911","impliedFormat":1},{"version":"1042064ece5bb47d6aba91648fbe0635c17c600ebdf567588b4ca715602f0a9d","impliedFormat":1},{"version":"4360ad4de54de2d5c642c4375d5eab0e7fe94ebe8adca907e6c186bbef75a54d","impliedFormat":1},{"version":"c338dff3233675f87a3869417aaea8b8bf590505106d38907dc1d0144f6402ef","impliedFormat":1},{"version":"7bb79aa2fead87d9d56294ef71e056487e848d7b550c9a367523ee5416c44cfa","impliedFormat":1},{"version":"9c9cae45dc94c2192c7d25f80649414fa13c425d0399a2c7cb2b979e4e50af42","impliedFormat":1},{"version":"068f063c2420b20f8845afadb38a14c640aed6bb01063df224edb24af92b4550","impliedFormat":1},{"version":"27ff4196654e6373c9af16b6165120e2dd2169f9ad6abb5c935af5abd8c7938c","impliedFormat":1},{"version":"b8719d4483ebef35e9cb67cd5677b7e0103cf2ed8973df6aba6fdd02896ddc6e","impliedFormat":1},{"version":"643672ce383e1c58ea665a92c5481f8441edbd3e91db36e535abccbc9035adeb","impliedFormat":1},{"version":"6dd9bcf10678b889842d467706836a0ab42e6c58711e33918ed127073807ee65","impliedFormat":1},{"version":"8fa022ea514ce0ea78ac9b7092a9f97f08ead20c839c779891019e110fce8307","impliedFormat":1},{"version":"c93235337600b786fd7d0ff9c71a00f37ca65c4d63e5d695fc75153be2690f09","impliedFormat":1},{"version":"10179c817a384983f6925f778a2dac2c9427817f7d79e27d3e9b1c8d0564f1f4","impliedFormat":1},{"version":"ce791f6ea807560f08065d1af6014581eeb54a05abd73294777a281b6dfd73c2","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"c0a666b005521f52e2db0b685d659d7ee9b0b60bc0d347dfc5e826c7957bdb83","impliedFormat":1},{"version":"807d38d00ce6ab9395380c0f64e52f2f158cc804ac22745d8f05f0efdec87c33","impliedFormat":1},{"version":"ce0df82a9ae6f914ba08409d4d883983cc08e6d59eb2df02d8e4d68309e7848b","impliedFormat":1},{"version":"796273b2edc72e78a04e86d7c58ae94d370ab93a0ddf40b1aa85a37a1c29ecd7","impliedFormat":1},{"version":"5df15a69187d737d6d8d066e189ae4f97e41f4d53712a46b2710ff9f8563ec9f","impliedFormat":1},{"version":"e17cd049a1448de4944800399daa4a64c5db8657cc9be7ef46be66e2a2cd0e7c","impliedFormat":1},{"version":"d05fb434f4ba073aed74b6c62eff1723c835de2a963dbb091e000a2decb5a691","impliedFormat":1},{"version":"10e6166be454ddb8c81000019ce1069b476b478c316e7c25965a91904ec5c1e3","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"4d4927cbee21750904af7acf940c5e3c491b4d5ebc676530211e389dd375607a","impliedFormat":1},{"version":"72105519d0390262cf0abe84cf41c926ade0ff475d35eb21307b2f94de985778","impliedFormat":1},{"version":"703989a003790524b4e34a1758941d05c121d5d352bccca55a5cfb0c76bca592","impliedFormat":1},{"version":"a58abf1f5c8feb335475097abeddd32fd71c4dc2065a3d28cf15cacabad9654a","impliedFormat":1},{"version":"ccf6dd45b708fb74ba9ed0f2478d4eb9195c9dfef0ff83a6092fa3cf2ff53b4f","impliedFormat":1},{"version":"2d7db1d73456e8c5075387d4240c29a2a900847f9c1bff106a2e490da8fbd457","impliedFormat":1},{"version":"2b15c805f48e4e970f8ec0b1915f22d13ca6212375e8987663e2ef5f0205e832","impliedFormat":1},{"version":"671aeae7130038566a8d00affeb1b3e3b131edf93cbcfff6f55ed68f1ca4c1b3","impliedFormat":1},{"version":"f0f05149debcf31b3a717ce8dd16e0323a789905cb9e27239167b604153b8885","impliedFormat":1},{"version":"35069c2c417bd7443ae7c7cafd1de02f665bf015479fec998985ffbbf500628c","impliedFormat":1},{"version":"955c69dde189d5f47a886ed454ff50c69d4d8aaec3a454c9ab9c3551db727861","impliedFormat":1},{"version":"cec8b16ff98600e4f6777d1e1d4ddf815a5556a9c59bc08cc16db4fd4ae2cf00","impliedFormat":1},{"version":"9e21f8e2c0cfea713a4a372f284b60089c0841eb90bf3610539d89dbcd12d65a","impliedFormat":1},{"version":"045b752f44bf9bbdcaffd882424ab0e15cb8d11fa94e1448942e338c8ef19fba","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"c226288bda11cee97850f0149cc4ff5a244d42ed3f5a9f6e9b02f1162bf1e3f4","impliedFormat":1},{"version":"210a4ec6fd58f6c0358e68f69501a74aef547c82deb920c1dec7fa04f737915a","impliedFormat":1},{"version":"8eea4cc42d04d26bcbcaf209366956e9f7abaf56b0601c101016bb773730c5fe","impliedFormat":1},{"version":"f5319e38724c54dff74ee734950926a745c203dcce00bb0343cb08fbb2f6b546","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"12b8dfed70961bea1861e5d39e433580e71323abb5d33da6605182ec569db584","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"7e560f533aaf88cf9d3b427dcf6c112dd3f2ee26d610e2587583b6c354c753db","impliedFormat":1},{"version":"71e0082342008e4dfb43202df85ea0986ef8e003c921a1e49999d0234a3019da","impliedFormat":1},{"version":"27ab780875bcbb65e09da7496f2ca36288b0c541abaa75c311450a077d54ec15","impliedFormat":1},{"version":"2652448ac55a2010a1f71dd141f828b682298d39728f9871e1cdf8696ef443fd","impliedFormat":1},{"version":"e71e103fb212e015394def7f1379706fce637fec9f91aa88410a73b7c5cbd4e3","impliedFormat":1},{"version":"120599fd965257b1f4d0ff794bc696162832d9d8467224f4665f713a3119078b","impliedFormat":1},{"version":"5433f33b0a20300cca35d2f229a7fc20b0e8477c44be2affeb21cb464af60c76","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"bd4131091b773973ca5d2326c60b789ab1f5e02d8843b3587effe6e1ea7c9d86","impliedFormat":1},{"version":"794998dc1c5a19ce77a75086fe829fb9c92f2fd07b5631c7d5e0d04fd9bc540c","impliedFormat":1},{"version":"2b0b12d0ee52373b1e7b09226eae8fbf6a2043916b7c19e2c39b15243f32bde2","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"0427df5c06fafc5fe126d14b9becd24160a288deff40e838bfbd92a35f8d0d00","impliedFormat":1},{"version":"bdc5fd605a6d315ded648abf2c691a22d0b0c774b78c15512c40ddf138e51950","impliedFormat":1},{"version":"49c346823ba6d4b12278c12c977fb3a31c06b9ca719015978cb145eb86da1c61","impliedFormat":1},{"version":"bfac6e50eaa7e73bb66b7e052c38fdc8ccfc8dbde2777648642af33cf349f7f1","impliedFormat":1},{"version":"92f7c1a4da7fbfd67a2228d1687d5c2e1faa0ba865a94d3550a3941d7527a45d","impliedFormat":1},{"version":"f53b120213a9289d9a26f5af90c4c686dd71d91487a0aa5451a38366c70dc64b","impliedFormat":1},{"version":"6cd4b0986c638d92f7204d1407b1cb3e0a79d7a2d23b0f141c1a0829540ce7ef","impliedFormat":1},{"version":"57d67b72e06059adc5e9454de26bbfe567d412b962a501d263c75c2db430f40e","impliedFormat":1},{"version":"6511e4503cf74c469c60aafd6589e4d14d5eb0a25f9bf043dcbecdf65f261972","impliedFormat":1},{"version":"d58265e159fc3cb30aa8878ba5e986a314b1759c824ff66d777b9fe42117231a","impliedFormat":1},{"version":"ff8fccaae640b0bb364340216dcc7423e55b6bb182ca2334837fee38636ad32e","impliedFormat":1},{"version":"a67b87d0281c97dfc1197ef28dfe397fc2c865ccd41f7e32b53f647184cc7307","impliedFormat":1},{"version":"771ffb773f1ddd562492a6b9aaca648192ac3f056f0e1d997678ff97dbb6bf9b","impliedFormat":1},{"version":"232f70c0cf2b432f3a6e56a8dc3417103eb162292a9fd376d51a3a9ea5fbbf6f","impliedFormat":1},{"version":"59ee66cf96b093b18c90a8f6dbb3f0e3b65c758fba7b8b980af9f2726c32c1a2","impliedFormat":1},{"version":"c590195790d7fa35b4abed577a605d283b8336b9e01fa9bf4ae4be49855940f9","impliedFormat":1},{"version":"8a0e762ceb20c7e72504feef83d709468a70af4abccb304f32d6b9bac1129b2c","impliedFormat":1},{"version":"026a43d8239b8f12d2fc4fa5a7acbc2ad06dd989d8c71286d791d9f57ca22b78","impliedFormat":1},{"version":"9252d498a77517aab5d8d4b5eb9d71e4b225bbc7123df9713e08181de63180f6","impliedFormat":1},{"version":"14cf3683955f914b4695e92c93aae5f3fe1e60f3321d712605164bfe53b34334","impliedFormat":1},{"version":"12f0fb50e28b9d48fe5b7580580efe7cc0bd38e4b8c02d21c175aa9a4fd839b0","impliedFormat":1},{"version":"1fffe726740f9787f15b532e1dc870af3cd964dbe29e191e76121aa3dd8693f2","impliedFormat":1},{"version":"7cd657e359eac7829db5f02c856993e8945ffccc71999cdfb4ab3bf801a1bbc6","impliedFormat":1},{"version":"1a82deef4c1d39f6882f28d275cad4c01f907b9b39be9cbc472fcf2cf051e05b","impliedFormat":1},{"version":"4b20fcf10a5413680e39f5666464859fc56b1003e7dfe2405ced82371ebd49b6","impliedFormat":1},{"version":"29c2aa0712786a4a504fce3acd50928f086027276f7490965cb467d2ce638bae","impliedFormat":1},{"version":"f14e63395b54caecc486f00a39953ab00b7e4d428a4e2c38325154b08eb5dcc2","impliedFormat":1},{"version":"e749bbd37dadf82c9833278780527c717226e1e2c9bc7b2576c8ec1c40ec5647","impliedFormat":1},{"version":"46e4e179b295f08d0bd0176fe44cf6c89558c9091d3cb3894f9eaaa42ea1add1","impliedFormat":1},{"version":"08f52a9edaabeda3b2ea19a54730174861ceed637c5ca1c1b0c39459fdc0853e","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"bb37588926aba35c9283fe8d46ebf4e79ffe976343105f5c6d45f282793352b2","impliedFormat":1},{"version":"05c97cddbaf99978f83d96de2d8af86aded9332592f08ce4a284d72d0952c391","impliedFormat":1},{"version":"72179f9dd22a86deaad4cc3490eb0fe69ee084d503b686985965654013f1391b","impliedFormat":1},{"version":"2e6114a7dd6feeef85b2c80120fdbfb59a5529c0dcc5bfa8447b6996c97a69f5","impliedFormat":1},{"version":"7b6ff760c8a240b40dab6e4419b989f06a5b782f4710d2967e67c695ef3e93c4","impliedFormat":1},{"version":"29164fb428c851bc35b632761daad3ae075993a0bf9c43e9e3bc6468b32d9aa5","impliedFormat":1},{"version":"3c01539405051bffccacffd617254c8d0f665cdce00ec568c6f66ccb712b734f","impliedFormat":1},{"version":"ef9021bdfe54f4df005d0b81170bd2da9bfd86ef552cde2a049ba85c9649658f","impliedFormat":1},{"version":"17a1a0d1c492d73017c6e9a8feb79e9c8a2d41ef08b0fe51debc093a0b2e9459","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"50256e9c31318487f3752b7ac12ff365c8949953e04568009c8705db802776fb","impliedFormat":1},{"version":"96e1caae9b78cde35c62fee46c1ec9fa5f12c16bc1e2ab08d48e5921e29a6958","impliedFormat":1},{"version":"8de9fe97fa9e00ec00666fa77ab6e91b35d25af8ca75dabcb01e14ad3299b150","impliedFormat":1},{"version":"9e0327857503a958348d9e8e9dd57ed155a1e6ec0071eb5eb946fe06ccdf7680","impliedFormat":1},{"version":"6c800b281b9e89e69165fd11536195488de3ff53004e55905e6c0059a2d8591e","impliedFormat":1},{"version":"01aa917531e116485beca44a14970834687b857757159769c16b228eb1e49c5f","impliedFormat":1},{"version":"397f568f996f8ffcf12d9156342552b0da42f6571eadba6bce61c99e1651977d","impliedFormat":1},{"version":"e2fd426f3cbc5bbff7860378784037c8fa9c1644785eed83c47c902b99b6cda9","impliedFormat":1},{"version":"d663134457d8d669ae0df34eabd57028bddc04fc444c4bc04bc5215afc91e1f4","impliedFormat":1},{"version":"a52674bc98da7979607e0f44d4c015c59c1b1d264c83fc50ec79ff2cfea06723","impliedFormat":1},{"version":"bcca16e60015db8bbf6bd117e88c5f7269337aebb05fc2b0701ae658a458c9c3","impliedFormat":1},{"version":"5e1246644fab20200cdc7c66348f3c861772669e945f2888ef58b461b81e1cd8","impliedFormat":1},{"version":"eb39550e2485298d91099e8ab2a1f7b32777d9a5ba34e9028ea8df2e64891172","impliedFormat":1},{"version":"e108f38a04a607f9386d68a4c6f3fdae1b712960f11f6482c6f1769bab056c2e","impliedFormat":1},{"version":"a3128a84a9568762a2996df79717d92154d18dd894681fc0ab3a098fa7f8ee3b","affectsGlobalScope":true,"impliedFormat":1},{"version":"347791f3792f436950396dd6171d6450234358001ae7c94ca209f1406566ccbf","impliedFormat":1},{"version":"dd80b1e600d00f5c6a6ba23f455b84a7db121219e68f89f10552c54ba46e4dc9","impliedFormat":1},{"version":"714d8ebb298c7acc9bd1f34bd479c57d12b73371078a0c5a1883a68b8f1b9389","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","impliedFormat":1},{"version":"51bf55bb6eb80f11b3aa59fb0a9571565a7ea304a19381f6da5630f4b2e206c4","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"98a787be42bd92f8c2a37d7df5f13e5992da0d967fab794adbb7ee18370f9849","impliedFormat":1},{"version":"02f8ef78d46c5b27f108dbb56709daa0aff625c20247abb0e6bb67cd73439f9f","impliedFormat":1},{"version":"b7fff2d004c5879cae335db8f954eb1d61242d9f2d28515e67902032723caeab","impliedFormat":1},{"version":"5f3dc10ae646f375776b4e028d2bed039a93eebbba105694d8b910feebbe8b9c","impliedFormat":1},{"version":"bb0cd7862b72f5eba39909c9889d566e198fcaddf7207c16737d0c2246112678","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"6812502cc640de74782ce9121592ae3765deb1c5c8e795b179736b308dd65e90","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"2b664c3cc544d0e35276e1fb2d4989f7d4b4027ffc64da34ec83a6ccf2e5c528","impliedFormat":1},{"version":"a3f41ed1b4f2fc3049394b945a68ae4fdefd49fa1739c32f149d32c0545d67f5","impliedFormat":1},{"version":"bad68fd0401eb90fe7da408565c8aee9c7a7021c2577aec92fa1382e8876071a","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"fec01479923e169fb52bd4f668dbeef1d7a7ea6e6d491e15617b46f2cacfa37d","impliedFormat":1},{"version":"8a8fb3097ba52f0ae6530ec6ab34e43e316506eb1d9aa29420a4b1e92a81442d","impliedFormat":1},{"version":"44e09c831fefb6fe59b8e65ad8f68a7ecc0e708d152cfcbe7ba6d6080c31c61e","impliedFormat":1},{"version":"1c0a98de1323051010ce5b958ad47bc1c007f7921973123c999300e2b7b0ecc0","impliedFormat":1},{"version":"b10bc147143031b250dc36815fd835543f67278245bf2d0a46dca765f215124e","impliedFormat":1},{"version":"87affad8e2243635d3a191fa72ef896842748d812e973b7510a55c6200b3c2a4","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"1e4c6ac595b6d734c056ac285b9ee50d27a2c7afe7d15bd14ed16210e71593b0","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"330896c1a2b9693edd617be24fbf9e5895d6e18c7955d6c08f028f272b37314d","impliedFormat":1},{"version":"1d9c0a9a6df4e8f29dc84c25c5aa0bb1da5456ebede7a03e03df08bb8b27bae6","impliedFormat":1},{"version":"84380af21da938a567c65ef95aefb5354f676368ee1a1cbb4cae81604a4c7d17","impliedFormat":1},{"version":"1af3e1f2a5d1332e136f8b0b95c0e6c0a02aaabd5092b36b64f3042a03debf28","impliedFormat":1},{"version":"3c7b3aecd652169787b3c512d8f274a3511c475f84dcd6cead164e40cad64480","impliedFormat":1},{"version":"9a01f12466488eccd8d9eafc8fecb9926c175a4bf4a8f73a07c3bcf8b3363282","impliedFormat":1},{"version":"b80f624162276f24a4ec78b8e86fbee80ca255938e12f8b58e7a8f1a6937120b","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"5bf5c7a44e779790d1eb54c234b668b15e34affa95e78eada73e5757f61ed76a","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"5c634644d45a1b6bc7b05e71e05e52ec04f3d73d9ac85d5927f647a5f965181a","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"b95a6f019095dd1d48fd04965b50dfd63e5743a6e75478343c46d2582a5132bf","impliedFormat":99},{"version":"c2008605e78208cfa9cd70bd29856b72dda7ad89df5dc895920f8e10bcb9cd0a","impliedFormat":99},{"version":"b97cb5616d2ab82a98ec9ada7b9e9cabb1f5da880ec50ea2b8dc5baa4cbf3c16","impliedFormat":99},{"version":"63a7595a5015e65262557f883463f934904959da563b4f788306f699411e9bac","impliedFormat":1},{"version":"4ba137d6553965703b6b55fd2000b4e07ba365f8caeb0359162ad7247f9707a6","impliedFormat":1},{"version":"00b0f43b3770f66aa1e105327980c0ff17a868d0e5d9f5689f15f8d6bf4fb1f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"272a7e7dbe05e8aaba1662ef1a16bbd57975cc352648b24e7a61b7798f3a0ad7","affectsGlobalScope":true,"impliedFormat":1},{"version":"a1219ee18b9282b4c6a31f1f0bcc9255b425e99363268ba6752a932cf76662f0","impliedFormat":1},{"version":"3dc14e1ab45e497e5d5e4295271d54ff689aeae00b4277979fdd10fa563540ae","impliedFormat":1},{"version":"1d63055b690a582006435ddd3aa9c03aac16a696fac77ce2ed808f3e5a06efab","impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","impliedFormat":1},"f2b3bca04d1bfe583daae1e1f798c92ec24bb6693bd88d0a09ba6802dee362a8",{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","impliedFormat":1},{"version":"cc4a4903fb698ca1d961d4c10dce658aa3a479faf40509d526f122b044eaf6a4","impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","impliedFormat":1},{"version":"370bde134aa8c2abc926d0e99d3a4d5d5dba65c6ee65459137e4f02670cbf841","impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","impliedFormat":1},{"version":"2977b7884aedc895a1d0c9c210c7cf3272c29d6959a08a6fa3ff71e0aff08175","impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","impliedFormat":1},{"version":"e3cf0611709328b449ec13f8c436712d62003620ce480139fae46ce001c2ee9f","impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","impliedFormat":99},{"version":"b558c9a18ea4e6e4157124465c3ef1063e64640da139e67be5edb22f534f2f08","impliedFormat":1},{"version":"01374379f82be05d25c08d2f30779fa4a4c41895a18b93b33f14aeef51768692","impliedFormat":1},{"version":"b0dee183d4e65cf938242efaf3d833c6b645afb35039d058496965014f158141","impliedFormat":1},{"version":"c0bbbf84d3fbd85dd60d040c81e8964cc00e38124a52e9c5dcdedf45fea3f213","impliedFormat":1},"8ab2020159a945e311929abd23acd6b9e2ad9b665bf31a7247f2e12ccad7662d",{"version":"751764bb94219b4ce8f5475dc35d3de2e432fea01a0c9610cd7f69ad05e398c6","impliedFormat":1},{"version":"ee70b8037ecdf0de6c04f35277f253663a536d7e38f1539d270e4e916d225a3f","affectsGlobalScope":true,"impliedFormat":1},{"version":"a660aa95476042d3fdcc1343cf6bb8fdf24772d31712b1db321c5a4dcc325434","impliedFormat":1},{"version":"282f98006ed7fa9bb2cd9bdbe2524595cfc4bcd58a0bb3232e4519f2138df811","impliedFormat":1},{"version":"6222e987b58abfe92597e1273ad7233626285bc2d78409d4a7b113d81a83496b","impliedFormat":1},{"version":"cbe726263ae9a7bf32352380f7e8ab66ee25b3457137e316929269c19e18a2be","impliedFormat":1},{"version":"8b96046bf5fb0a815cba6b0880d9f97b7f3a93cf187e8dcfe8e2792e97f38f87","impliedFormat":99},{"version":"bacf2c84cf448b2cd02c717ad46c3d7fd530e0c91282888c923ad64810a4d511","affectsGlobalScope":true,"impliedFormat":1},{"version":"82e687ebd99518bc63ea04b0c3810fb6e50aa6942decd0ca6f7a56d9b9a212a6","impliedFormat":99},{"version":"7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","impliedFormat":1},{"version":"8f07f2b6514744ac96e51d7cb8518c0f4de319471237ea10cf688b8d0e9d0225","impliedFormat":1},{"version":"257b83faa134d971c738a6b9e4c47e59bb7b23274719d92197580dd662bfafc3","impliedFormat":99},{"version":"d2e64a6f25013b099e83bfadb2c388d7bef3e8f3fdb25528225bbc841e7e7e3a","impliedFormat":99},{"version":"369ba5259e66ca8c7d35e3234f7a2a0863a770fdb8266505747c65cf346a0804","impliedFormat":99},{"version":"64d984f55025daf604f670b7dfd090ea765f2098aee871174ef2ee3e94479098","impliedFormat":99},{"version":"f147b6710441cf3ec3234adf63b0593ce5e8c9b692959d21d3babc8454bcf743","impliedFormat":99},{"version":"e96d5373a66c2cfbbc7e6642cf274055aa2c7ff6bd37be7480c66faf9804db6d","impliedFormat":99},{"version":"02bcdd7a76c5c1c485cbf05626d24c86ac8f9a1d8dc31f8924108bbaa4cf3ba9","impliedFormat":99},{"version":"c874ab6feac6e0fdf9142727c9a876065777a5392f14b0bbcf869b1e69eb46b5","impliedFormat":99},{"version":"7c553fc9e34773ddbaabe0fa1367d4b109101d0868a008f11042bee24b5a925d","impliedFormat":99},{"version":"9962ce696fbdce2421d883ca4b062a54f982496625437ae4d3633376c5ad4a80","impliedFormat":99},{"version":"e3ea467c4a7f743f3548c9ed61300591965b1d12c08c8bb9aaff8a002ba95fce","impliedFormat":99},{"version":"4c17183a07a63bea2653fbfc0a942b027160ddbee823024789a415f9589de327","impliedFormat":99},{"version":"3e2203c892297ea44b87470fde51b3d48cfe3eeb6901995de429539462894464","impliedFormat":99},{"version":"c84bf7a4abc5e7fdf45971a71b25b0e0d34ccd5e720a866dd78bb71d60d41a3f","impliedFormat":99},{"version":"e01ea380015ed698c3c0e2ccd0db72f3fc3ef1abc4519f122aa1c1a8d419a505","impliedFormat":99},{"version":"5ada1f8a9580c0f7478fe03ae3e07e958f0b79bdfb9dd50eeb98c1324f40011b","impliedFormat":99},{"version":"a8301dc90b4bd9fba333226ee0f1681aeeff1bd90233a8f647e687cb4b7d3521","impliedFormat":99},{"version":"e3225dc0bec183183509d290f641786245e6652bc3dce755f7ef404060693c35","impliedFormat":99},{"version":"09a03870ed8c55d7453bc9ad684df88965f2f770f987481ca71b8a09be5205bc","impliedFormat":99},{"version":"e6233e1c976265e85aa8ad76c3881febe6264cb06ae3136f0257e1eab4a6cc5a","impliedFormat":99},{"version":"2cdd50ddc49e2d608ee848fc4ab0db9a2716624fabb4209c7c683d87e54d79c5","impliedFormat":99},{"version":"e431d664338b8470abb1750d699c7dfcebb1a25434559ef85bb96f1e82de5972","impliedFormat":99},{"version":"2c4254139d037c3caca66ce291c1308c1b5092cfcb151eb25980db932dd3b01a","impliedFormat":99},{"version":"970ae00ed018cb96352dc3f37355ef9c2d9f8aa94d7174ccd6d0ed855e462097","impliedFormat":99},{"version":"d2f8dee457ef7660b604226d471d55d927c3051766bdd80353553837492635c3","impliedFormat":99},{"version":"110a503289a2ef76141ffff3ffceb9a1c3662c32748eb9f6777a2bd0866d6fb1","impliedFormat":99},{"version":"69bf2422313487956e4dacf049f30cb91b34968912058d244cb19e4baa24da97","impliedFormat":99},{"version":"310e6b62c493ce991624169a1c1904015769d947be88dc67e00adc7ebebcfa87","impliedFormat":99},{"version":"62fefda288160bf6e435b21cc03d3fbac11193d8d3bd0e82d86623cca7691c29","impliedFormat":99},{"version":"fcc46a8bcbf9bef21023bba1995160a25f0bc590ca3563ec44c315b4f4c1b18a","impliedFormat":99},{"version":"669573548930fb7d0a0761b827e203dc623581e21febf0be80fb02414f217d74","impliedFormat":99},{"version":"f974db5be6d7428044e19c2848e72495a9b56a8d33b6fcab392e7fb5328eb8df","impliedFormat":99},{"version":"aa348c4fb2f8ac77df855f07fb66281c9f6e71746fdff3b13c7932aa7642b788","impliedFormat":99},{"version":"262d4c61b446d096f0615b5acb0a09007b2a425c6a0fd38b66e404e7b6021812","signature":"4b96dd19fd2949d28ce80e913412b0026dc421e5bf6c31d87c7b5eb11b5753b4"},{"version":"0309a01650023994ed96edbd675ea4fdc3779a823ce716ad876cc77afb792b62","impliedFormat":99},{"version":"f13d7beeea58e219daef3a40e0dc4f2bd7d9581ac04cedec236102a12dfd2090","impliedFormat":99},{"version":"48c411efce1848d1ed55de41d7deb93cbf7c04080912fd87aa517ed25ef42639","affectsGlobalScope":true,"impliedFormat":1},{"version":"a094636c05f3e75cb072684dd42cd25a4c1324bec4a866706c85c04cecd49613","affectsGlobalScope":true,"impliedFormat":99},{"version":"fe2d63fcfdde197391b6b70daf7be8c02a60afa90754a5f4a04bdc367f62793d","impliedFormat":99},{"version":"9a3e2c85ec1ab7a0874a19814cc73c691b716282cb727914093089c5a8475955","impliedFormat":99},{"version":"cbdc781d2429935c9c42acd680f2a53a9f633e8de03290ec6ea818e4f7bff19a","impliedFormat":99},{"version":"9f6d9f5dd710922f82f69abf9a324e28122b5f31ae6f6ce78427716db30a377e","impliedFormat":99},{"version":"ac2414a284bdecfd6ab7b87578744ab056cd04dd574b17853cd76830ef5b72f2","impliedFormat":99},{"version":"c3f921bbc9d2e65bd503a56fbc66da910e68467baedb0b9db0cc939e1876c0d7","impliedFormat":99},{"version":"c30a41267fc04c6518b17e55dcb2b810f267af4314b0b6d7df1c33a76ce1b330","impliedFormat":1},{"version":"72422d0bac4076912385d0c10911b82e4694fc106e2d70added091f88f0824ba","impliedFormat":1},{"version":"da251b82c25bee1d93f9fd80c5a61d945da4f708ca21285541d7aff83ecb8200","impliedFormat":1},{"version":"64db14db2bf37ac089766fdb3c7e1160fabc10e9929bc2deeede7237e4419fc8","impliedFormat":1},{"version":"98b94085c9f78eba36d3d2314affe973e8994f99864b8708122750788825c771","impliedFormat":1},{"version":"0cc99fbb161d78729d71fad66c6c363e3095862d6277160f29fa960744b785c6","affectsGlobalScope":true,"impliedFormat":99},"129a86faf021cbd7dd75371cef3025e3eb2137128b7cd35ea06b566f3413315c",{"version":"61de421bab744b4e1e105a40e0409221ba5729868cde52fbdbe3e21a1fb2c69c","impliedFormat":99},{"version":"6c84d9410cb39c418e76c74200dc943c8909863efe798df05c552933801dc42f","impliedFormat":99},"2f2b0c92621875746bfc2466ec57dc31ea32d7150682ddc7c8448b34e254462e",{"version":"3aa67bb15606c55fdd705fc720d1c5550078cb83f21355806a72495e8f59cdfa","signature":"07415405713faaca7975ad1bfbf9b412ba297f47a6923fe9cebafd7f4dcb7395"},"6a9a61013d77df9d48372dd9f6da3b2b7395ca4675b4fb4cfbce80060beb8d86",{"version":"f86904596a26a20f8a8e5ea2aa7784549bcb3b8cb8d5e3f25cec4aee552ede5e","signature":"97e658c85b782da4a23f4616189c629aad1f20746b0741d62e34087c3fd6d275"},{"version":"8f250abfe6d907fb058b0bc4c6d40be322ecc1ab6743db6b9f05a2b582f02341","signature":"76629349cc645a06e85c6df552397bd0ca3f0e09e7b11fdd2a56d89fbcc92309"},{"version":"0f407ed5c25d2fb2c97a216d175a3efc0fe21fd0faf4de3cca00931016375991","signature":"46f78a03a7c6287bd7eaa736b490ad45be142f90896a3d02a739501b3d5b2de0"},{"version":"ce2c97f3e5b6a222f19f005b6ef4886d89bf4c16bc1903658802d04a552fc9d8","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cdec8f8e76fb1dfd3a6fdca604aae2bf3f51c1110f7a0012dc5604fdbb12f689","signature":"e79eaa521c0e7a44cd476d4c4ec7a2150ffd2800d3f4a92e0c4d32eee476849b"},"af7de680832f9a99aa122282a09b27ce3ca195b913239ed42775e10797136a9d",{"version":"59dd6a66b9027174bc2de83cd568fc135fa912f806adc7a2fe2eee15bdb869b8","signature":"b03385fb2f787f4429edb0274994492d1ffa934d91d88f01b5744c44686ff5ce"},{"version":"51d79ddaeeb46de8db2aa5928bf15a830b33ff0b91050c548a2ea82831010b60","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"84113bd518e3f94aa08d774a4a2088305059e2d14c1c3978b44ddb912b47f535","signature":"7c0e72bec073fd2eac188dc7d8ee98116783df0c8d7c478d14bdd2eb8d9ae108"},{"version":"429c3079ec8ef32bca7a44c2516b710e417795ce92887a8daa92072aa73a3279","signature":"a08f08cd9b2d0864fdba81ce3528f18c0ef1ca4e2a9e114d1e235c0f7a6d2d76"},{"version":"26f2cea077ea007aacd5a6702cdd76616c3eddf5bdd329e9f88258875ffa8121","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"54d753331fcddf49d121fe9d5320d2d2d14ec161504e15d7370562fd631bc957","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"4caa4cac2a1e939f9a5d8b39dd20a2f318f01881a3f3864a5c5af3a1529db9c1","signature":"0746e1c710ea512e3f96f75811f718cc809bfb06a1bbbacaa2d0bfa71ffa930b"},{"version":"1dd5285b8f9e9e0b1417c1b07aff574aee793d72a87fee897b0001d48031a2df","signature":"8f0d9f945e58c09ce6189be2303fd068f5a66272e5d9778f230ceb6d15fb8c5b"},{"version":"c5a0ffa909f5277c3830ab0ea463558b135ea049b731b3852310e0b64ca3869f","signature":"40d5d8eeb71a7cfb02078d06351f4467bdf5d8dc4d47ba467480eb30b9b73e94"},{"version":"c0efe9f1a7841088fd3b0cdb9c2290a6f5c1fa7a3aebdf3aaf270d344ff56e21","signature":"7175ca141479166d4eb613d64104eeed84093343cca30afd909912737339e111"},{"version":"ff0d9317f266d554abc684692d66c0a4cc76c2ed22248400d7838032dea5d84c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"662ff9b16378365dce8ac7752370c8c8609f11c6f5d53480c5dad6bdc00a467d","signature":"b3f301ee5acbe3a478e4b2fc5c86b1eb021f624afbf2295dbc0c605cd052de16"},"fa452635b5ac56ff89f7c2bef9ae560435d24d8f50f4ba9e661dee9e50475d28","1a017381422d08696696ee73d0b477f65817ceb7db6a862c0ee1274ec8ebf19b",{"version":"ab3408c1be7894e73630304d2c8aebea85542ca23fb96a7011032812bb2f3f90","signature":"ba54342937eb116567785cc93f6ef7c8ce3b041a67bb2ecb579ab28359b06047"},{"version":"c7fd9ee637fa81986028569225441584268f0d1a00b8d42cfcf9de738a881da1","signature":"950dcb0d1b29e2ef981b7bc2d893f177a18c2d731510b2aab0aa59e2b25ca7c0"},{"version":"74a1b2fc1d585437ddadf00f90ac46fc005e41679621625cbea5f5fe868c6f30","signature":"824201568ccc90c3008484b7f5db8b7ff9563b6ab086836e9de560ee0cfe45ab"},"532ebc18ccfd0dde7741c8b3063a4d38117516b46b83b92a79587713b8e64d67","1adc127175e94440e797af519f6778fcbbd7965dbff102f9c2bb8059b3650e28",{"version":"1414615c866abf5fa5557c370fd8744a06a31e6c4403c713e17272fa2db15a91","signature":"e27f6810eed02e88d63146c2e843a57ba7846571669afc3c6a9871dc5f66d300"},{"version":"87efe6a08272fd6ad1f8c21c2c55b64cd713a48daf94a12e14ab4030944b7ba3","signature":"0f32d061b698a5b6715f2fb0bd9235bb1b1be541361ccbb9ef3075cfc661ef4e"},{"version":"304fd3f360a445e635986b3264ee8017e6a3631a2a4f6faf0d0bf7d3f98e62eb","signature":"4523fcdd7dc2a00ac6c7d5c02b0e11288069c09dc1eb5e472259cc165193351e"},{"version":"5470908d24bb9a7d1bd87b5f06733e685aadaf77b8ade7c96bc210e5050c4a5d","signature":"a533121eee3903d4cc40d812648fb68cf6f62d670913ef28f5f65a179e4ca6a8"},{"version":"187c0cd60326c19be31746d530c8082dcc0b395da934fddd17ad4a356c9a417b","signature":"272b05835aacc3bab10b150e1cbbe13b0878f49f64f027cae57c19906d07469c"},{"version":"d74845f8b6f02aff4b3dd36b99003dd4fe58ac2376ae511f6d92bcb3a153d6aa","signature":"6781825fe0bacb6828c87c307a6126fe3a0ae631d079cb04b8b852804fdaa7bb"},{"version":"a63d1dceca466019e671006f8f071c0bf5596206b98557239ca64f9e58c191e0","signature":"3b31470d2936e80d198c801cc596e0569736a32b31f5544a6453f3934e24fba7"},{"version":"4763b947677a33bd554af8624d932aedea06bcf57a25b11ef6a7e057210358d6","signature":"6cf67db3bb1d66e8d7dbe762fed6bdf98d8236cc313c69a42dffae94476336ab"},{"version":"251c70669857e6de36884f09be038138bbc8111e5d597f33d3910df9cf4f5ae6","signature":"db8a0682e6c1d96fd0116f63cde511bd7b3de4387312aabd7da4bf53d72e0ad6"},"60193090d4e861a6cc875a6f00b0616a024ac586b866dfbdf09e4ca053aa8c65","efff1f1a0df5cb30394a6323bf9fad9532cf4284de8ca3820b8c49d8bd88b463","b9aeaade08803f4f2f5e1fcf82527451afc68b6c93896a749d27d92da239344a","9da27bb4af2cd79a55068f5fa60724e96145ecd54835420f3769c1aeca2a8ebd","56900c830691def7957f3b487b19d91136aa05af0752f83673b2204303c19152","ff25d2789578679f3f1b1f5dcbb5c87a0dad6488fa583cd1fded8ae83c58b1b5",{"version":"30cdf53273913e8ee8b04cabd23d6b53df2f92265a9c88470e5d7c957d95b7ef","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},"dc575c9e3b32d510c9a974135f13f83e2b836d8bb482310c40b2495b0a763dd2",{"version":"abac25862a735319832d29b4daa0eac0af8d63e7a9f6dd0d95dcb687d8cab4a6","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},"6e2133f8ad52b25f2ce9c7ccc30fdd2653214949e08552ee3fbd970716d07dc2","0d7c3b7a0922f5a70e863e5dc809850c9a42d9438ab0ee919657845b3db88c18","a9f7e7f88d04c20cb51988a6c5bceaf00920c6d7034f094106613a6a1f1e3160"],"root":[449,474,519,536,[539,587]],"options":{"allowJs":true,"esModuleInterop":true,"jsx":1,"module":99,"skipLibCheck":true,"strict":true,"target":9},"referencedMap":[[582,1],[583,2],[584,3],[585,4],[586,5],[587,6],[581,7],[562,8],[566,9],[571,10],[576,11],[578,12],[580,13],[564,14],[563,15],[565,16],[577,17],[567,18],[568,19],[569,20],[570,21],[579,22],[546,23],[573,24],[572,24],[574,24],[575,25],[536,26],[541,27],[540,24],[539,24],[543,28],[542,29],[545,30],[544,24],[547,31],[549,32],[548,18],[552,33],[551,34],[553,35],[550,24],[554,36],[555,37],[556,24],[558,38],[557,24],[559,39],[561,40],[560,18],[449,41],[395,24],[475,24],[121,42],[122,42],[123,43],[76,44],[124,45],[125,46],[126,47],[71,24],[74,48],[72,24],[73,24],[127,49],[128,50],[129,51],[130,52],[131,53],[132,54],[133,54],[134,55],[135,56],[136,57],[137,58],[77,24],[75,24],[138,59],[139,60],[140,61],[174,62],[141,63],[142,24],[143,64],[144,65],[145,66],[146,67],[147,68],[148,69],[149,70],[150,71],[151,72],[152,72],[153,73],[154,24],[155,74],[156,75],[158,76],[157,77],[159,78],[160,79],[161,80],[162,81],[163,82],[164,83],[165,84],[166,85],[167,86],[168,87],[169,88],[170,89],[171,90],[78,24],[79,24],[80,24],[118,91],[119,24],[120,24],[172,92],[173,93],[178,94],[179,95],[177,23],[175,96],[176,97],[60,24],[62,98],[291,23],[522,24],[523,99],[524,100],[527,101],[526,24],[487,24],[498,102],[493,103],[496,104],[511,105],[500,24],[503,106],[502,107],[514,107],[501,108],[525,24],[495,109],[497,109],[489,110],[492,111],[508,110],[494,112],[488,24],[61,24],[482,24],[532,113],[534,114],[533,115],[531,116],[530,24],[69,117],[398,118],[403,7],[405,119],[199,120],[347,121],[374,122],[274,24],[192,24],[197,24],[338,123],[266,124],[198,24],[376,125],[377,126],[319,127],[335,128],[239,129],[342,130],[343,131],[341,132],[340,24],[339,133],[375,134],[200,135],[273,24],[275,136],[195,24],[210,137],[201,138],[214,137],[243,137],[185,137],[346,139],[356,24],[191,24],[297,140],[298,141],[292,142],[426,24],[300,24],[301,142],[293,143],[430,144],[429,145],[425,24],[379,24],[334,146],[333,24],[424,147],[294,23],[217,148],[215,149],[427,24],[428,24],[216,150],[419,151],[422,152],[226,153],[225,154],[224,155],[433,23],[223,156],[261,24],[436,24],[439,24],[438,23],[440,157],[181,24],[344,158],[345,159],[368,24],[190,160],[180,24],[183,161],[313,23],[312,162],[311,163],[302,24],[303,24],[310,24],[305,24],[308,164],[304,24],[306,165],[309,166],[307,165],[196,24],[188,24],[189,137],[397,167],[406,168],[410,169],[350,170],[349,24],[258,24],[441,171],[359,172],[295,173],[296,174],[288,175],[280,24],[286,24],[287,176],[317,177],[281,178],[318,179],[315,180],[314,24],[316,24],[270,181],[351,182],[352,183],[282,184],[283,185],[278,186],[330,187],[358,188],[361,189],[259,190],[186,191],[357,192],[182,122],[380,193],[391,194],[378,24],[390,195],[70,24],[366,196],[246,24],[276,197],[362,24],[205,24],[389,198],[194,24],[249,199],[348,200],[388,24],[382,201],[187,24],[383,202],[385,203],[386,204],[369,24],[387,191],[213,205],[367,206],[392,207],[322,24],[325,24],[323,24],[327,24],[324,24],[326,24],[328,208],[321,24],[252,209],[251,24],[257,210],[253,211],[256,212],[255,212],[254,211],[209,213],[241,214],[355,215],[442,24],[414,216],[416,217],[285,24],[415,218],[353,182],[299,182],[193,24],[242,219],[206,220],[207,221],[208,222],[204,223],[329,223],[220,223],[244,224],[221,224],[203,225],[202,24],[250,226],[248,227],[247,228],[245,229],[354,230],[290,231],[320,232],[289,233],[337,234],[336,235],[332,236],[238,237],[240,238],[237,239],[211,240],[269,24],[402,24],[268,241],[331,24],[260,242],[279,243],[277,244],[262,245],[264,246],[437,24],[263,247],[265,247],[400,24],[399,24],[401,24],[435,24],[267,248],[235,23],[68,24],[218,249],[227,24],[272,250],[212,24],[408,23],[418,251],[234,23],[412,142],[233,252],[394,253],[232,251],[184,24],[420,254],[230,23],[231,23],[222,24],[271,24],[229,255],[228,256],[219,257],[284,71],[360,71],[384,24],[364,258],[363,24],[404,24],[236,23],[396,259],[63,23],[66,260],[67,261],[64,23],[65,24],[381,262],[373,263],[372,24],[371,264],[370,24],[393,265],[407,266],[409,267],[411,268],[413,269],[417,270],[448,271],[421,271],[447,272],[423,273],[431,274],[432,275],[434,276],[443,277],[446,160],[445,24],[444,278],[538,279],[537,24],[466,280],[464,281],[465,282],[453,283],[454,281],[461,284],[452,285],[457,286],[467,24],[458,287],[463,288],[469,289],[468,290],[451,291],[459,292],[460,293],[455,294],[462,280],[456,295],[477,296],[476,297],[365,298],[450,24],[472,299],[471,24],[470,24],[473,300],[512,24],[490,24],[491,301],[58,24],[59,24],[10,24],[11,24],[13,24],[12,24],[2,24],[14,24],[15,24],[16,24],[17,24],[18,24],[19,24],[20,24],[21,24],[3,24],[22,24],[23,24],[4,24],[24,24],[28,24],[25,24],[26,24],[27,24],[29,24],[30,24],[31,24],[5,24],[32,24],[33,24],[34,24],[35,24],[6,24],[39,24],[36,24],[37,24],[38,24],[40,24],[7,24],[41,24],[46,24],[47,24],[42,24],[43,24],[44,24],[45,24],[8,24],[51,24],[48,24],[49,24],[50,24],[52,24],[9,24],[53,24],[54,24],[55,24],[57,24],[56,24],[1,24],[96,302],[106,303],[95,302],[116,304],[87,305],[86,306],[115,278],[109,307],[114,308],[89,309],[103,310],[88,311],[112,312],[84,313],[83,278],[113,314],[85,315],[90,316],[91,24],[94,316],[81,24],[117,317],[107,318],[98,319],[99,320],[101,321],[97,322],[100,323],[110,278],[92,324],[93,325],[102,326],[82,327],[105,318],[104,316],[108,24],[111,328],[509,329],[506,330],[507,329],[510,331],[505,24],[486,332],[483,333],[481,334],[479,335],[478,24],[480,336],[484,24],[485,337],[518,338],[513,339],[504,340],[499,24],[528,341],[515,342],[529,343],[516,344],[521,345],[520,346],[517,347],[535,348],[474,349],[519,350]],"affectedFilesPendingEmit":[582,583,584,585,586,587,562,566,571,576,578,580,564,563,565,577,567,568,569,570,579,546,573,572,574,575,536,541,540,543,542,545,544,547,549,548,552,551,553,550,554,555,556,558,557,559,561,560,474,519],"version":"5.9.3"} \ No newline at end of file +{"fileNames":["./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.regexp.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.decorators.legacy.d.ts","./node_modules/.pnpm/@types+react@19.0.0/node_modules/@types/react/global.d.ts","./node_modules/.pnpm/csstype@3.2.3/node_modules/csstype/index.d.ts","./node_modules/.pnpm/@types+react@19.0.0/node_modules/@types/react/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/styled-jsx/types/css.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/styled-jsx/types/macro.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/styled-jsx/types/style.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/styled-jsx/types/global.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/styled-jsx/types/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/amp.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/amp.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/get-page-files.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/compatibility/disposable.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/compatibility/indexable.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/compatibility/iterators.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/compatibility/index.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/globals.typedarray.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/buffer.buffer.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/globals.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/web-globals/abortcontroller.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/web-globals/domexception.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/web-globals/events.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/header.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/readable.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/file.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/fetch.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/formdata.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/connector.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/client.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/errors.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/dispatcher.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-dispatcher.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/global-origin.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool-stats.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/pool.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/handlers.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/balanced-pool.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/agent.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-interceptor.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-agent.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-client.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-pool.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/mock-errors.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/proxy-agent.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/env-http-proxy-agent.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-handler.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/retry-agent.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/api.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/interceptors.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/util.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cookies.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/patch.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/websocket.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/eventsource.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/filereader.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/diagnostics-channel.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/content-type.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/cache.d.ts","./node_modules/.pnpm/undici-types@6.21.0/node_modules/undici-types/index.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/web-globals/fetch.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/web-globals/navigator.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/web-globals/storage.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/assert.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/assert/strict.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/async_hooks.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/buffer.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/child_process.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/cluster.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/console.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/constants.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/crypto.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/dgram.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/diagnostics_channel.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/dns.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/dns/promises.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/domain.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/events.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/fs.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/fs/promises.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/http.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/http2.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/https.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/inspector.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/inspector.generated.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/module.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/net.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/os.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/path.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/perf_hooks.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/process.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/punycode.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/querystring.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/readline.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/readline/promises.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/repl.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/sea.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/sqlite.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/stream.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/stream/promises.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/stream/consumers.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/stream/web.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/string_decoder.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/test.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/timers.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/timers/promises.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/tls.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/trace_events.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/tty.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/url.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/util.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/v8.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/vm.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/wasi.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/worker_threads.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/zlib.d.ts","./node_modules/.pnpm/@types+node@22.19.21/node_modules/@types/node/index.d.ts","./node_modules/.pnpm/@types+react@19.0.0/node_modules/@types/react/canary.d.ts","./node_modules/.pnpm/@types+react@19.0.0/node_modules/@types/react/experimental.d.ts","./node_modules/.pnpm/@types+react-dom@19.0.0/node_modules/@types/react-dom/index.d.ts","./node_modules/.pnpm/@types+react-dom@19.0.0/node_modules/@types/react-dom/canary.d.ts","./node_modules/.pnpm/@types+react-dom@19.0.0/node_modules/@types/react-dom/experimental.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/fallback.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/webpack/webpack.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/config.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/load-custom-routes.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/image-config.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/plugins/subresource-integrity-plugin.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/body-streams.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/revalidate.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/setup-exception-listeners.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/worker.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/constants.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/app-router-headers.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/rendering-mode.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/require-hook.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/experimental/ppr.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/plugins/app-build-manifest-plugin.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/page-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/segment-config/app/app-segment-config.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/segment-config/pages/pages-segment-config.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/analysis/get-page-static-info.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/loaders/get-module-build-info.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/plugins/middleware-plugin.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-kind.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-definitions/route-definition.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-definitions/app-page-route-definition.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/cache-handlers/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/response-cache/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/resume-data-cache/cache-store.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/resume-data-cache/resume-data-cache.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/render-result.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/plugins/flight-manifest-plugin.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/route-module.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/deep-readonly.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/load-components.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/plugins/next-font-manifest-plugin.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/router-reducer/router-reducer-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/flight-data-helpers.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/router-reducer/fetch-server-response.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/app-router-context.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router/utils/middleware-route-matcher.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-definitions/locale-route-definition.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-definitions/pages-route-definition.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/mitt.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/with-router.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/router.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/route-loader.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/page-loader.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/bloom-filter.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router/router.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router-context.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/loadable-context.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/loadable.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/image-config-context.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/hooks-client-context.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/head-manager-context.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/amp-context.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/server-inserted-html.shared-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/pages/vendored/contexts/entrypoints.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/pages/module.compiled.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/templates/pages.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/pages/module.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/render.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/response-cache/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/plugins/pages-manifest-plugin.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-definitions/pages-api-route-definition.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-matches/pages-api-route-match.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/instrumentation/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-matchers/route-matcher.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-matcher-providers/route-matcher-provider.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/i18n-provider.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-matcher-managers/route-matcher-manager.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/normalizers/normalizer.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/normalizers/locale-route-normalizer.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/normalizers/request/pathname-normalizer.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/normalizers/request/suffix.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/normalizers/request/rsc.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/normalizers/request/prefetch-rsc.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/normalizers/request/next-data.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/after/builtin-request-context.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/base-server.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/next-url.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/@edge-runtime/cookies/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/cookies.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/request.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/fetch-event.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/response.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/segment-config/middleware/middleware-config.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/adapter.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/use-cache/cache-life.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/modern-browserslist-target.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/constants.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/loaders/metadata/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/page-extensions-type.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack/loaders/next-app-loader/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/app-dir-module.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/adapters/request-cookies.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/async-storage/draft-mode-provider.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/adapters/headers.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/cache-signal.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/dynamic-rendering.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/work-unit-async-storage-instance.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/work-unit-async-storage.external.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router/utils/parse-relative-url.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request/fallback-params.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/clean-async-snapshot-instance.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/clean-async-snapshot.external.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/app-render.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/app-page/vendored/contexts/entrypoints.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/app-page/module.compiled.d.ts","./node_modules/.pnpm/@types+react@19.0.0/node_modules/@types/react/jsx-runtime.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/error-boundary.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/layout-router.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/render-from-template-context.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/action-async-storage-instance.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/action-async-storage.external.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/client-page.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/client-segment.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request/search-params.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/hooks-server-context.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/http-access-fallback/error-boundary.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/alternative-urls-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/extra-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/metadata-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/manifest-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/opengraph-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/twitter-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/metadata-interface.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/resolvers.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/types/icons.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/resolve-metadata.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/metadata.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/metadata/metadata-boundary.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/rsc/preloads.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/rsc/postpone.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/rsc/taint.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/collect-segment-data.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/entry-base.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/templates/app-page.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/app-page/module.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-polyfill-crypto.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-environment-baseline.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-environment-extensions/error-inspect.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-environment-extensions/random.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-environment-extensions/date.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-environment-extensions/web-crypto.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-environment-extensions/node-crypto.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/node-environment.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-definitions/app-route-route-definition.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/async-storage/work-store.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/http.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/app-route/shared-modules.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/redirect-status-code.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/redirect-error.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/templates/app-route.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/app-route/module.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-modules/app-route/module.compiled.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/segment-config/app/app-segments.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/utils.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/turborepo-access-trace/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/turborepo-access-trace/result.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/turborepo-access-trace/helpers.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/turborepo-access-trace/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/export/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/export/worker.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/worker.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/incremental-cache/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/after/after.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/after/after-context.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/work-async-storage-instance.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/app-render/work-async-storage.external.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request/params.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/route-matches/route-match.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request-meta.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/cli/next-test.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/config-shared.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/base-http/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/api-utils/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router/utils/parse-url.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/base-http/node.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/async-callback-set.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router/utils/route-regex.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router/utils/route-matcher.d.ts","./node_modules/.pnpm/sharp@0.33.5/node_modules/sharp/lib/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/image-optimizer.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/next-server.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/lib/coalesced-function.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/router-utils/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/trace/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/trace/trace.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/trace/shared.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/trace/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/load-jsconfig.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/webpack-config.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/swc/generated-native.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/build/swc/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/dev/parse-version-info.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/react-dev-overlay/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/dev/hot-reloader-types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/telemetry/storage.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/render-server.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/router-server.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/router/utils/path-match.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/router-utils/filesystem.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/router-utils/setup-dev-bundler.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/lru-cache.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/lib/dev-bundler-service.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/dev/static-paths-worker.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/dev/next-dev-server.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/next.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/html-context.shared-runtime.d.ts","./node_modules/.pnpm/@next+env@15.1.3/node_modules/@next/env/dist/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/utils.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/pages/_app.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/app.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/unstable-cache.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/revalidate.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/unstable-no-store.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/use-cache/cache-tag.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/cache.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/runtime-config.external.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/config.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/pages/_document.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/document.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/dynamic.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dynamic.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/pages/_error.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/error.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/head.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/head.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request/cookies.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request/headers.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request/draft-mode.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/headers.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/get-img-props.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/image-component.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/shared/lib/image-external.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/image.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/link.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/link.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/redirect.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/not-found.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/forbidden.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/unauthorized.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/unstable-rethrow.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/navigation.react-server.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/components/navigation.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/navigation.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/router.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/client/script.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/script.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/user-agent.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/@edge-runtime/primitives/url.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/web/spec-extension/image-response.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/@vercel/og/satori/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/@vercel/og/emoji/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/compiled/@vercel/og/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/after/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/dist/server/request/connection.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/server.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/types/global.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/types/compiled.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/types.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/index.d.ts","./node_modules/.pnpm/next@15.1.3_react-dom@19.0.0_react@19.0.0__react@19.0.0/node_modules/next/image-types/global.d.ts","./next-env.d.ts","./node_modules/.pnpm/source-map-js@1.2.1/node_modules/source-map-js/source-map.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/previous-map.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/input.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/css-syntax-error.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/declaration.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/root.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/warning.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/lazy-result.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/no-work-result.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/processor.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/result.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/document.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/rule.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/node.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/comment.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/container.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/at-rule.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/list.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/postcss.d.ts","./node_modules/.pnpm/postcss@8.5.15/node_modules/postcss/lib/postcss.d.mts","./node_modules/.pnpm/tailwindcss@3.4.19/node_modules/tailwindcss/types/generated/corepluginlist.d.ts","./node_modules/.pnpm/tailwindcss@3.4.19/node_modules/tailwindcss/types/generated/colors.d.ts","./node_modules/.pnpm/tailwindcss@3.4.19/node_modules/tailwindcss/types/config.d.ts","./node_modules/.pnpm/tailwindcss@3.4.19/node_modules/tailwindcss/types/index.d.ts","./tailwind.config.ts","./node_modules/.pnpm/@types+estree@1.0.9/node_modules/@types/estree/index.d.ts","./node_modules/.pnpm/rollup@4.62.0/node_modules/rollup/dist/rollup.d.ts","./node_modules/.pnpm/rollup@4.62.0/node_modules/rollup/dist/parseast.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/types/hmrpayload.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/types/customevent.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/types/hot.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/dist/node/types.d-agj9qkwt.d.ts","./node_modules/.pnpm/esbuild@0.21.5/node_modules/esbuild/lib/main.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/dist/node/runtime.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/types/importglob.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/types/metadata.d.ts","./node_modules/.pnpm/vite@5.4.21_@types+node@22.19.21/node_modules/vite/dist/node/index.d.ts","./node_modules/.pnpm/@vitest+pretty-format@2.1.9/node_modules/@vitest/pretty-format/dist/index.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/types.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/helpers.d.ts","./node_modules/.pnpm/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/index-c1cfc5e9.d.ts","./node_modules/.pnpm/tinyrainbow@1.2.0/node_modules/tinyrainbow/dist/node.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/index.d.ts","./node_modules/.pnpm/@vitest+runner@2.1.9/node_modules/@vitest/runner/dist/tasks-3znpj1lr.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/types-bxe-2udy.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/diff.d.ts","./node_modules/.pnpm/@vitest+runner@2.1.9/node_modules/@vitest/runner/dist/types.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/error.d.ts","./node_modules/.pnpm/@vitest+runner@2.1.9/node_modules/@vitest/runner/dist/index.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/environment.looobwuu.d.ts","./node_modules/.pnpm/@vitest+snapshot@2.1.9/node_modules/@vitest/snapshot/dist/environment-ddx0edty.d.ts","./node_modules/.pnpm/@vitest+snapshot@2.1.9/node_modules/@vitest/snapshot/dist/rawsnapshot-cpnkto81.d.ts","./node_modules/.pnpm/@vitest+snapshot@2.1.9/node_modules/@vitest/snapshot/dist/index.d.ts","./node_modules/.pnpm/@vitest+snapshot@2.1.9/node_modules/@vitest/snapshot/dist/environment.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/config.cy0c388z.d.ts","./node_modules/.pnpm/vite-node@2.1.9_@types+node@22.19.21/node_modules/vite-node/dist/trace-mapping.d-dlvdeqop.d.ts","./node_modules/.pnpm/vite-node@2.1.9_@types+node@22.19.21/node_modules/vite-node/dist/index-z0r8hvru.d.ts","./node_modules/.pnpm/vite-node@2.1.9_@types+node@22.19.21/node_modules/vite-node/dist/index.d.ts","./node_modules/.pnpm/@vitest+utils@2.1.9/node_modules/@vitest/utils/dist/source-map.d.ts","./node_modules/.pnpm/vite-node@2.1.9_@types+node@22.19.21/node_modules/vite-node/dist/client.d.ts","./node_modules/.pnpm/vite-node@2.1.9_@types+node@22.19.21/node_modules/vite-node/dist/server.d.ts","./node_modules/.pnpm/@vitest+runner@2.1.9/node_modules/@vitest/runner/dist/utils.d.ts","./node_modules/.pnpm/tinybench@2.9.0/node_modules/tinybench/dist/index.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/benchmark.geerunq4.d.ts","./node_modules/.pnpm/@vitest+snapshot@2.1.9/node_modules/@vitest/snapshot/dist/manager.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/reporters.nr4dxcka.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/vite.czkp4x9w.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/config.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/config.d.ts","./vitest.config.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/worker.tn5kgiih.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/worker.b9fxpcac.d.ts","./node_modules/.pnpm/@vitest+expect@2.1.9/node_modules/@vitest/expect/dist/chai.d.cts","./node_modules/.pnpm/@vitest+expect@2.1.9/node_modules/@vitest/expect/dist/index.d.ts","./node_modules/.pnpm/@vitest+expect@2.1.9/node_modules/@vitest/expect/index.d.ts","./node_modules/.pnpm/@vitest+spy@2.1.9/node_modules/@vitest/spy/dist/index.d.ts","./node_modules/.pnpm/@vitest+mocker@2.1.9_vite@5.4.21_@types+node@22.19.21_/node_modules/@vitest/mocker/dist/types-dzoqtgin.d.ts","./node_modules/.pnpm/@vitest+mocker@2.1.9_vite@5.4.21_@types+node@22.19.21_/node_modules/@vitest/mocker/dist/index.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/mocker.crtm890j.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/chunks/suite.b2jumifp.d.ts","./node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/utils.d.ts","./node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/overloads.d.ts","./node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/branding.d.ts","./node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/messages.d.ts","./node_modules/.pnpm/expect-type@1.3.0/node_modules/expect-type/dist/index.d.ts","./node_modules/.pnpm/vitest@2.1.9_@types+node@22.19.21/node_modules/vitest/dist/index.d.ts","./lib/api/client.test.ts","./node_modules/.pnpm/openapi-typescript-helpers@0.0.15/node_modules/openapi-typescript-helpers/index.d.ts","./node_modules/.pnpm/openapi-fetch@0.13.8/node_modules/openapi-fetch/dist/index.d.ts","./lib/api/schema.d.ts","./lib/api/config.ts","./lib/api/client.ts","./lib/api/types.ts","./lib/api/server.ts","./lib/autosave/autosave.ts","./lib/autosave/autosave.test.ts","./components/toast.tsx","./lib/autosave/useautosave.ts","./lib/foreshadow/board.ts","./lib/foreshadow/board.test.ts","./lib/foreshadow/useforeshadow.ts","./lib/outline/outline.ts","./lib/outline/outline.test.ts","./lib/outline/useoutline.ts","./lib/pace/beatmap.ts","./lib/pace/beatmap.test.ts","./lib/review/decisions.ts","./lib/review/decisions.test.ts","./lib/review/sse.ts","./lib/review/history.ts","./lib/review/history.test.ts","./lib/review/sse.test.ts","./lib/review/useaccept.ts","./lib/review/usereviewstream.ts","./lib/settings/providers.ts","./lib/stream/sse.ts","./lib/stream/sse.test.ts","./lib/stream/usedraftstream.ts","./lib/wizard/wizard.ts","./lib/wizard/wizard.test.ts","./app/layout.tsx","./components/leftnav.tsx","./components/appshell.tsx","./components/projectcard.tsx","./app/page.tsx","./components/foreshadow/foreshadowcard.tsx","./components/foreshadow/kanbancolumn.tsx","./components/foreshadow/registerform.tsx","./components/foreshadow/foreshadowboard.tsx","./app/projects/[id]/foreshadow/page.tsx","./components/outline/outlinechapterrow.tsx","./components/outline/outlineeditor.tsx","./app/projects/[id]/outline/page.tsx","./components/review/acceptpanel.tsx","./components/review/annotatedtext.tsx","./components/review/conflictcard.tsx","./components/review/foreshadowsuggestions.tsx","./components/review/beatmap.tsx","./components/review/pacepanel.tsx","./components/review/reviewreport.tsx","./app/projects/[id]/review/page.tsx","./components/workbench/chapterlist.tsx","./components/workbench/chapterassistant.tsx","./components/workbench/editor.tsx","./components/workbench/workbench.tsx","./app/projects/[id]/write/page.tsx","./components/projectwizard.tsx","./app/projects/new/page.tsx","./components/settings/providerssettings.tsx","./app/settings/providers/page.tsx","./.next/types/cache-life.d.ts","./.next/types/app/layout.ts","./.next/types/app/page.ts","./.next/types/app/projects/[id]/foreshadow/page.ts","./.next/types/app/projects/[id]/outline/page.ts","./.next/types/app/projects/[id]/review/page.ts","./.next/types/app/projects/[id]/write/page.ts","./.next/types/app/projects/new/page.ts","./.next/types/app/settings/providers/page.ts"],"fileIdsList":[[76,124,141,142,308,570],[76,124,141,142,308,574],[76,124,141,142,308,579],[76,124,141,142,308,582],[76,124,141,142,308,590],[76,124,141,142,308,595],[76,124,141,142,308,597],[76,124,141,142,308,599],[76,124,141,142,399,400,401,402],[76,124,141,142,447,546],[76,124,141,142,423,542,543,572,573],[76,124,141,142,431,543,578],[76,124,141,142,431,543,581],[76,124,141,142,431,543,559,589],[76,124,141,142,431,543,594],[76,124,141,142,572,596],[76,124,141,142,542,543,572,598],[62,76,124,141,142,423,571],[62,76,124,141,142,542,548,550,572,576,577],[62,76,124,141,142,542,548],[76,124,141,142,542,548,575],[62,76,124,141,142,548],[76,124,141,142,423,431],[76,124,141,142,542,551],[62,76,124,141,142,542,551,553,572,580],[76,124,141,142,423,542],[62,76,124,141,142,431,541,546,568],[76,124,141,142,542],[76,124,141,142,558],[76,124,141,142,554],[76,124,141,142,556,558],[76,124,141,142,558,587],[62,76,124,141,142,542,556,558,559,562,563,572,583,584,585,586,588],[62,76,124,141,142,541,542,546,564],[62,76,124,141,142],[76,124,141,142],[62,76,124,141,142,423,542,547,567,572,591,592,593],[76,124,141,142,535],[76,124,141,142,538,539,540],[76,124,141,142,540,542],[76,124,141,142,539],[76,124,141,142,535,544],[62,76,124,141,142,541,544,546],[76,124,141,142,535,542,548],[62,76,124,141,142,541,542,546,548],[76,124,141,142,535,542,551],[62,76,124,141,142,541,542,546],[76,124,141,142,535,554],[76,124,141,142,535,556],[76,124,141,142,535,542,559],[76,124,141,142,542,558],[76,124,141,142,535,558],[62,76,124,141,142,541,542,546,556],[62,76,124,141,142,540,558],[76,124,141,142,535,565],[62,76,124,141,142,540,565],[76,124,141,142,535,568],[76,124,141,142,447,448],[76,121,122,124,141,142],[76,123,124,141,142],[124,141,142],[76,124,129,141,142,159],[76,124,125,130,135,141,142,144,156,167],[76,124,125,126,135,141,142,144],[71,72,73,76,124,141,142],[76,124,127,141,142,168],[76,124,128,129,136,141,142,145],[76,124,129,141,142,156,164],[76,124,130,132,135,141,142,144],[76,123,124,131,141,142],[76,124,132,133,141,142],[76,124,134,135,141,142],[76,123,124,135,141,142],[76,124,135,136,137,141,142,156,167],[76,124,135,136,137,141,142,151,156,159],[76,117,124,132,135,138,141,142,144,156,167],[76,124,135,136,138,139,141,142,144,156,164,167],[76,124,138,140,141,142,156,164,167],[74,75,76,77,78,79,80,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173],[76,124,135,141,142],[76,124,141,142,143,167],[76,124,132,135,141,142,144,156],[76,124,141,142,145],[76,124,141,142,146],[76,123,124,141,142,147],[76,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173],[76,124,141,142,149],[76,124,141,142,150],[76,124,135,141,142,151,152],[76,124,141,142,151,153,168,170],[76,124,136,141,142],[76,124,135,141,142,156,157,159],[76,124,141,142,158,159],[76,124,141,142,156,157],[76,124,141,142,159],[76,124,141,142,160],[76,121,124,141,142,156,161,167],[76,124,135,141,142,162,163],[76,124,141,142,162,163],[76,124,129,141,142,144,156,164],[76,124,141,142,165],[76,124,141,142,144,166],[76,124,138,141,142,150,167],[76,124,129,141,142,168],[76,124,141,142,156,169],[76,124,141,142,143,170],[76,124,141,142,171],[76,117,124,141,142],[76,117,124,135,137,141,142,147,156,159,167,169,170,172],[76,124,141,142,156,173],[62,76,124,141,142,177,179],[62,76,124,141,142,177,178],[62,66,76,124,141,142,176,393,440],[62,66,76,124,141,142,175,393,440],[60,61,76,124,141,142],[76,124,141,142,491,492,495],[76,124,141,142,523],[76,124,141,142,526],[76,124,141,142,492,493,495,496,497],[76,124,141,142,492],[76,124,141,142,492,493,495],[76,124,141,142,492,493],[76,124,141,142,500],[76,124,141,142,487,500,501],[76,124,141,142,487,500],[76,124,141,142,487,494],[76,124,141,142,488],[76,124,141,142,487,488,489,491],[76,124,141,142,487],[76,124,141,142,530,531],[76,124,141,142,530,531,532,533],[76,124,141,142,530,532],[76,124,141,142,530],[68,76,124,141,142],[76,124,141,142,397],[76,124,141,142,404],[76,124,141,142,183,196,197,198,200,357],[76,124,141,142,183,187,189,190,191,192,346,357,359],[76,124,141,142,357],[76,124,141,142,197,213,290,337,353],[76,124,141,142,183],[76,124,141,142,377],[76,124,141,142,357,359,376],[76,124,141,142,276,290,318,445],[76,124,141,142,283,300,337,352],[76,124,141,142,238],[76,124,141,142,341],[76,124,141,142,340,341,342],[76,124,141,142,340],[70,76,124,138,141,142,180,183,190,193,194,195,197,201,269,274,320,328,338,348,357,393],[76,124,141,142,183,199,227,272,357,373,374,445],[76,124,141,142,199,445],[76,124,141,142,272,273,274,357,445],[76,124,141,142,445],[76,124,141,142,183,199,200,445],[76,124,141,142,193,339,345],[76,124,141,142,150,291,353],[76,124,141,142,291,353],[62,76,124,141,142,291],[62,76,124,141,142,270,291,292],[76,124,141,142,218,236,353,429],[76,124,141,142,334,424,425,426,427,428],[76,124,141,142,333],[76,124,141,142,333,334],[76,124,141,142,191,215,216,270],[76,124,141,142,217,218,270],[76,124,141,142,270],[62,76,124,141,142,184,418],[62,76,124,141,142,167],[62,76,124,141,142,199,225],[62,76,124,141,142,199],[76,124,141,142,223,228],[62,76,124,141,142,224,396],[62,66,76,124,138,141,142,174,175,176,393,438,439],[76,124,136,138,141,142,187,213,241,259,270,343,357,358,445],[76,124,141,142,328,344],[76,124,141,142,393],[76,124,141,142,182],[76,124,141,142,150,276,288,309,311,352,353],[76,124,141,142,150,276,288,308,309,310,352,353],[76,124,141,142,302,303,304,305,306,307],[76,124,141,142,304],[76,124,141,142,308],[62,76,124,141,142,224,291,396],[62,76,124,141,142,291,394,396],[62,76,124,141,142,291,396],[76,124,141,142,259,349],[76,124,141,142,349],[76,124,138,141,142,358,396],[76,124,141,142,296],[76,123,124,141,142,295],[76,124,141,142,209,210,212,242,270,283,284,285,287,320,352,355,358],[76,124,141,142,286],[76,124,141,142,210,218,270],[76,124,141,142,283,352],[76,124,141,142,283,292,293,294,296,297,298,299,300,301,312,313,314,315,316,317,352,353,445],[76,124,141,142,281],[76,124,138,141,142,150,187,208,210,212,213,214,218,246,259,268,269,320,348,357,358,359,393,445],[76,124,141,142,352],[76,123,124,141,142,197,212,269,285,300,348,350,351,358],[76,124,141,142,283],[76,123,124,141,142,208,242,262,277,278,279,280,281,282],[76,124,138,141,142,262,263,277,358,359],[76,124,141,142,197,259,269,270,285,348,352,358],[76,124,138,141,142,357,359],[76,124,138,141,142,156,355,358,359],[76,124,138,141,142,150,167,180,187,199,209,210,212,213,214,219,241,242,243,245,246,249,250,252,255,256,257,258,270,347,348,353,355,357,358,359],[76,124,138,141,142,156],[76,124,141,142,183,184,185,187,194,355,356,393,396,445],[76,124,138,141,142,156,167,203,375,377,378,379,445],[76,124,141,142,150,167,180,203,213,242,243,250,259,267,270,348,353,355,360,361,367,373,389,390],[76,124,141,142,193,194,269,328,339,348,357],[76,124,138,141,142,167,184,242,355,357,365],[76,124,141,142,275],[76,124,138,141,142,386,387,388],[76,124,141,142,355,357],[76,124,141,142,187,212,242,347,396],[76,124,138,141,142,150,250,259,355,361,367,369,373,389,392],[76,124,138,141,142,193,328,373,382],[76,124,141,142,183,219,347,357,384],[76,124,138,141,142,199,219,357,368,369,380,381,383,385],[70,76,124,141,142,210,211,212,393,396],[76,124,138,141,142,150,167,187,193,201,209,213,214,242,243,245,246,258,259,267,270,328,347,348,353,354,355,360,361,362,364,366,396],[76,124,138,141,142,156,193,355,367,386,391],[76,124,141,142,323,324,325,326,327],[76,124,141,142,249,251],[76,124,141,142,253],[76,124,141,142,251],[76,124,141,142,253,254],[76,124,138,141,142,187,208,358],[62,76,124,138,141,142,150,182,184,187,209,210,212,213,214,240,355,359,393,396],[76,124,138,141,142,150,167,186,191,242,354,358],[76,124,141,142,277],[76,124,141,142,278],[76,124,141,142,279],[76,124,141,142,202,206],[76,124,138,141,142,187,202,209],[76,124,141,142,205,206],[76,124,141,142,207],[76,124,141,142,202,203],[76,124,141,142,202,220],[76,124,141,142,202],[76,124,141,142,248,249,354],[76,124,141,142,247],[76,124,141,142,203,353,354],[76,124,141,142,244,354],[76,124,141,142,203,353],[76,124,141,142,320],[76,124,141,142,204,209,211,242,270,276,285,288,289,319,355,358],[76,124,141,142,218,229,232,233,234,235,236],[76,124,141,142,336],[76,124,141,142,197,211,212,263,270,283,296,300,329,330,331,332,334,335,338,347,352,357],[76,124,141,142,218],[76,124,141,142,240],[76,124,138,141,142,209,211,221,237,239,241,355,393,396],[76,124,141,142,218,229,230,231,232,233,234,235,236,394],[76,124,141,142,203],[76,124,141,142,263,264,267,348],[76,124,138,141,142,249,357],[76,124,138,141,142],[76,124,141,142,262,283],[76,124,141,142,261],[76,124,141,142,258,263],[76,124,141,142,260,262,357],[76,124,138,141,142,186,263,264,265,266,357,358],[62,76,124,141,142,215,217,270],[76,124,141,142,271],[62,76,124,141,142,184],[62,76,124,141,142,353],[62,70,76,124,141,142,212,214,393,396],[76,124,141,142,184,418,419],[62,76,124,141,142,228],[62,76,124,141,142,150,167,182,222,224,226,227,396],[76,124,141,142,199,353,358],[76,124,141,142,353,363],[62,76,124,136,138,141,142,150,182,228,272,393,394,395],[62,76,124,141,142,175,176,393,440],[62,63,64,65,66,76,124,141,142],[76,124,129,141,142],[76,124,141,142,370,371,372],[76,124,141,142,370],[62,66,76,124,138,140,141,142,150,174,175,176,177,179,180,182,246,308,359,392,396,440],[76,124,141,142,406],[76,124,141,142,408],[76,124,141,142,410],[76,124,141,142,412],[76,124,141,142,414,415,416],[76,124,141,142,420],[67,69,76,124,141,142,398,403,405,407,409,411,413,417,421,423,431,432,434,443,444,445,446],[76,124,141,142,422],[76,124,141,142,430],[76,124,141,142,224],[76,124,141,142,433],[76,123,124,141,142,263,264,265,267,299,353,435,436,437,440,441,442],[76,124,141,142,174],[76,124,141,142,537],[76,124,141,142,465],[76,124,141,142,463,465],[76,124,141,142,454,462,463,464,466,468],[76,124,141,142,452],[76,124,141,142,455,460,465,468],[76,124,141,142,451,468],[76,124,141,142,455,456,459,460,461,468],[76,124,141,142,455,456,457,459,460,468],[76,124,141,142,452,453,454,455,456,460,461,462,464,465,466,468],[76,124,141,142,468],[76,124,141,142,450,452,453,454,455,456,457,459,460,461,462,463,464,465,466,467],[76,124,141,142,450,468],[76,124,141,142,455,457,458,460,461,468],[76,124,141,142,459,468],[76,124,141,142,460,461,465,468],[76,124,141,142,453,463],[76,124,141,142,476,485],[76,124,141,142,475,476],[76,124,141,142,156,174],[76,124,141,142,470,471],[76,124,141,142,469,472],[76,124,141,142,490],[76,89,93,124,141,142,167],[76,89,124,141,142,156,167],[76,84,124,141,142],[76,86,89,124,141,142,164,167],[76,124,141,142,144,164],[76,84,124,141,142,174],[76,86,89,124,141,142,144,167],[76,81,82,85,88,124,135,141,142,156,167],[76,89,96,124,141,142],[76,81,87,124,141,142],[76,89,110,111,124,141,142],[76,85,89,124,141,142,159,167,174],[76,110,124,141,142,174],[76,83,84,124,141,142,174],[76,89,124,141,142],[76,83,84,85,86,87,88,89,90,91,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,111,112,113,114,115,116,124,141,142],[76,89,104,124,141,142],[76,89,96,97,124,141,142],[76,87,89,97,98,124,141,142],[76,88,124,141,142],[76,81,84,89,124,141,142],[76,89,93,97,98,124,141,142],[76,93,124,141,142],[76,87,89,92,124,141,142,167],[76,81,86,89,96,124,141,142],[76,124,141,142,156],[76,84,89,110,124,141,142,172,174],[76,124,141,142,505,506],[76,124,141,142,505],[76,124,141,142,486,505,506,516],[76,124,135,136,138,139,140,141,142,144,156,164,167,173,174,469,476,477,478,479,480,481,482,483,484,485],[76,124,141,142,478,479,480,481],[76,124,141,142,478,479,480],[76,124,141,142,478],[76,124,141,142,479],[76,124,141,142,476],[76,124,141,142,517],[76,124,141,142,498,511,512,535],[76,124,141,142,487,498,502,503,535],[76,124,141,142,527],[76,124,136,141,142,156,486,487,492,498,499,502,504,507,508,509,510,513,514,516,522,535],[76,124,141,142,498,511,512,513,535],[76,124,141,142,486,515],[76,124,141,142,172,520],[76,124,141,142,498,499,502,504,507,535],[76,124,136,141,142,156,486,487,492,498,499,502,503,504,507,508,509,510,511,512,513,514,515,516,522,535],[76,124,136,141,142,156,172,486,487,492,495,498,499,502,503,504,507,508,509,510,511,512,513,514,515,516,520,521,522,524,525,527,528,529,534,535],[76,124,141,142,473],[76,124,141,142,146,518]],"fileInfos":[{"version":"c430d44666289dae81f30fa7b2edebf186ecc91a2d4c71266ea6ae76388792e1","affectsGlobalScope":true,"impliedFormat":1},{"version":"45b7ab580deca34ae9729e97c13cfd999df04416a79116c3bfb483804f85ded4","impliedFormat":1},{"version":"3facaf05f0c5fc569c5649dd359892c98a85557e3e0c847964caeb67076f4d75","impliedFormat":1},{"version":"e44bb8bbac7f10ecc786703fe0a6a4b952189f908707980ba8f3c8975a760962","impliedFormat":1},{"version":"5e1c4c362065a6b95ff952c0eab010f04dcd2c3494e813b493ecfd4fcb9fc0d8","impliedFormat":1},{"version":"68d73b4a11549f9c0b7d352d10e91e5dca8faa3322bfb77b661839c42b1ddec7","impliedFormat":1},{"version":"5efce4fc3c29ea84e8928f97adec086e3dc876365e0982cc8479a07954a3efd4","impliedFormat":1},{"version":"feecb1be483ed332fad555aff858affd90a48ab19ba7272ee084704eb7167569","impliedFormat":1},{"version":"ee7bad0c15b58988daa84371e0b89d313b762ab83cb5b31b8a2d1162e8eb41c2","impliedFormat":1},{"version":"080941d9f9ff9307f7e27a83bcd888b7c8270716c39af943532438932ec1d0b9","affectsGlobalScope":true,"impliedFormat":1},{"version":"2e80ee7a49e8ac312cc11b77f1475804bee36b3b2bc896bead8b6e1266befb43","affectsGlobalScope":true,"impliedFormat":1},{"version":"c57796738e7f83dbc4b8e65132f11a377649c00dd3eee333f672b8f0a6bea671","affectsGlobalScope":true,"impliedFormat":1},{"version":"dc2df20b1bcdc8c2d34af4926e2c3ab15ffe1160a63e58b7e09833f616efff44","affectsGlobalScope":true,"impliedFormat":1},{"version":"515d0b7b9bea2e31ea4ec968e9edd2c39d3eebf4a2d5cbd04e88639819ae3b71","affectsGlobalScope":true,"impliedFormat":1},{"version":"0559b1f683ac7505ae451f9a96ce4c3c92bdc71411651ca6ddb0e88baaaad6a3","affectsGlobalScope":true,"impliedFormat":1},{"version":"0dc1e7ceda9b8b9b455c3a2d67b0412feab00bd2f66656cd8850e8831b08b537","affectsGlobalScope":true,"impliedFormat":1},{"version":"ce691fb9e5c64efb9547083e4a34091bcbe5bdb41027e310ebba8f7d96a98671","affectsGlobalScope":true,"impliedFormat":1},{"version":"8d697a2a929a5fcb38b7a65594020fcef05ec1630804a33748829c5ff53640d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"4ff2a353abf8a80ee399af572debb8faab2d33ad38c4b4474cff7f26e7653b8d","affectsGlobalScope":true,"impliedFormat":1},{"version":"fb0f136d372979348d59b3f5020b4cdb81b5504192b1cacff5d1fbba29378aa1","affectsGlobalScope":true,"impliedFormat":1},{"version":"d15bea3d62cbbdb9797079416b8ac375ae99162a7fba5de2c6c505446486ac0a","affectsGlobalScope":true,"impliedFormat":1},{"version":"68d18b664c9d32a7336a70235958b8997ebc1c3b8505f4f1ae2b7e7753b87618","affectsGlobalScope":true,"impliedFormat":1},{"version":"eb3d66c8327153d8fa7dd03f9c58d351107fe824c79e9b56b462935176cdf12a","affectsGlobalScope":true,"impliedFormat":1},{"version":"38f0219c9e23c915ef9790ab1d680440d95419ad264816fa15009a8851e79119","affectsGlobalScope":true,"impliedFormat":1},{"version":"69ab18c3b76cd9b1be3d188eaf8bba06112ebbe2f47f6c322b5105a6fbc45a2e","affectsGlobalScope":true,"impliedFormat":1},{"version":"a680117f487a4d2f30ea46f1b4b7f58bef1480456e18ba53ee85c2746eeca012","affectsGlobalScope":true,"impliedFormat":1},{"version":"2f11ff796926e0832f9ae148008138ad583bd181899ab7dd768a2666700b1893","affectsGlobalScope":true,"impliedFormat":1},{"version":"4de680d5bb41c17f7f68e0419412ca23c98d5749dcaaea1896172f06435891fc","affectsGlobalScope":true,"impliedFormat":1},{"version":"954296b30da6d508a104a3a0b5d96b76495c709785c1d11610908e63481ee667","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac9538681b19688c8eae65811b329d3744af679e0bdfa5d842d0e32524c73e1c","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a969edff4bd52585473d24995c5ef223f6652d6ef46193309b3921d65dd4376","affectsGlobalScope":true,"impliedFormat":1},{"version":"9e9fbd7030c440b33d021da145d3232984c8bb7916f277e8ffd3dc2e3eae2bdb","affectsGlobalScope":true,"impliedFormat":1},{"version":"811ec78f7fefcabbda4bfa93b3eb67d9ae166ef95f9bff989d964061cbf81a0c","affectsGlobalScope":true,"impliedFormat":1},{"version":"717937616a17072082152a2ef351cb51f98802fb4b2fdabd32399843875974ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"d7e7d9b7b50e5f22c915b525acc5a49a7a6584cf8f62d0569e557c5cfc4b2ac2","affectsGlobalScope":true,"impliedFormat":1},{"version":"71c37f4c9543f31dfced6c7840e068c5a5aacb7b89111a4364b1d5276b852557","affectsGlobalScope":true,"impliedFormat":1},{"version":"576711e016cf4f1804676043e6a0a5414252560eb57de9faceee34d79798c850","affectsGlobalScope":true,"impliedFormat":1},{"version":"89c1b1281ba7b8a96efc676b11b264de7a8374c5ea1e6617f11880a13fc56dc6","affectsGlobalScope":true,"impliedFormat":1},{"version":"74f7fa2d027d5b33eb0471c8e82a6c87216223181ec31247c357a3e8e2fddc5b","affectsGlobalScope":true,"impliedFormat":1},{"version":"d6d7ae4d1f1f3772e2a3cde568ed08991a8ae34a080ff1151af28b7f798e22ca","affectsGlobalScope":true,"impliedFormat":1},{"version":"063600664504610fe3e99b717a1223f8b1900087fab0b4cad1496a114744f8df","affectsGlobalScope":true,"impliedFormat":1},{"version":"934019d7e3c81950f9a8426d093458b65d5aff2c7c1511233c0fd5b941e608ab","affectsGlobalScope":true,"impliedFormat":1},{"version":"52ada8e0b6e0482b728070b7639ee42e83a9b1c22d205992756fe020fd9f4a47","affectsGlobalScope":true,"impliedFormat":1},{"version":"3bdefe1bfd4d6dee0e26f928f93ccc128f1b64d5d501ff4a8cf3c6371200e5e6","affectsGlobalScope":true,"impliedFormat":1},{"version":"59fb2c069260b4ba00b5643b907ef5d5341b167e7d1dbf58dfd895658bda2867","affectsGlobalScope":true,"impliedFormat":1},{"version":"639e512c0dfc3fad96a84caad71b8834d66329a1f28dc95e3946c9b58176c73a","affectsGlobalScope":true,"impliedFormat":1},{"version":"368af93f74c9c932edd84c58883e736c9e3d53cec1fe24c0b0ff451f529ceab1","affectsGlobalScope":true,"impliedFormat":1},{"version":"af3dd424cf267428f30ccfc376f47a2c0114546b55c44d8c0f1d57d841e28d74","affectsGlobalScope":true,"impliedFormat":1},{"version":"995c005ab91a498455ea8dfb63aa9f83fa2ea793c3d8aa344be4a1678d06d399","affectsGlobalScope":true,"impliedFormat":1},{"version":"959d36cddf5e7d572a65045b876f2956c973a586da58e5d26cde519184fd9b8a","affectsGlobalScope":true,"impliedFormat":1},{"version":"965f36eae237dd74e6cca203a43e9ca801ce38824ead814728a2807b1910117d","affectsGlobalScope":true,"impliedFormat":1},{"version":"3925a6c820dcb1a06506c90b1577db1fdbf7705d65b62b99dce4be75c637e26b","affectsGlobalScope":true,"impliedFormat":1},{"version":"0a3d63ef2b853447ec4f749d3f368ce642264246e02911fcb1590d8c161b8005","affectsGlobalScope":true,"impliedFormat":1},{"version":"8cdf8847677ac7d20486e54dd3fcf09eda95812ac8ace44b4418da1bbbab6eb8","affectsGlobalScope":true,"impliedFormat":1},{"version":"8444af78980e3b20b49324f4a16ba35024fef3ee069a0eb67616ea6ca821c47a","affectsGlobalScope":true,"impliedFormat":1},{"version":"3287d9d085fbd618c3971944b65b4be57859f5415f495b33a6adc994edd2f004","affectsGlobalScope":true,"impliedFormat":1},{"version":"b4b67b1a91182421f5df999988c690f14d813b9850b40acd06ed44691f6727ad","affectsGlobalScope":true,"impliedFormat":1},{"version":"8e7f8264d0fb4c5339605a15daadb037bf238c10b654bb3eee14208f860a32ea","affectsGlobalScope":true,"impliedFormat":1},{"version":"782dec38049b92d4e85c1585fbea5474a219c6984a35b004963b00beb1aab538","affectsGlobalScope":true,"impliedFormat":1},{"version":"36a2e4c9a67439aca5f91bb304611d5ae6e20d420503e96c230cf8fcdc948d94","affectsGlobalScope":true,"impliedFormat":1},{"version":"ac51dd7d31333793807a6abaa5ae168512b6131bd41d9c5b98477fc3b7800f9f","impliedFormat":1},{"version":"a305ee2f90e34e9e70aba9a9e9a154ce20c4d5cd1499cd21b8dc3617e1e5c810","impliedFormat":1},{"version":"acd8fd5090ac73902278889c38336ff3f48af6ba03aa665eb34a75e7ba1dccc4","impliedFormat":1},{"version":"d6258883868fb2680d2ca96bc8b1352cab69874581493e6d52680c5ffecdb6cc","impliedFormat":1},{"version":"1b61d259de5350f8b1e5db06290d31eaebebc6baafd5f79d314b5af9256d7153","impliedFormat":1},{"version":"f258e3960f324a956fc76a3d3d9e964fff2244ff5859dcc6ce5951e5413ca826","impliedFormat":1},{"version":"643f7232d07bf75e15bd8f658f664d6183a0efaca5eb84b48201c7671a266979","impliedFormat":1},{"version":"0f6666b58e9276ac3a38fdc80993d19208442d6027ab885580d93aec76b4ef00","impliedFormat":1},{"version":"05fd364b8ef02fb1e174fbac8b825bdb1e5a36a016997c8e421f5fab0a6da0a0","impliedFormat":1},{"version":"631eff75b0e35d1b1b31081d55209abc43e16b49426546ab5a9b40bdd40b1f60","impliedFormat":1},{"version":"6c7176368037af28cb72f2392010fa1cef295d6d6744bca8cfb54985f3a18c3e","affectsGlobalScope":true,"impliedFormat":1},{"version":"ab41ef1f2cdafb8df48be20cd969d875602483859dc194e9c97c8a576892c052","affectsGlobalScope":true,"impliedFormat":1},{"version":"437e20f2ba32abaeb7985e0afe0002de1917bc74e949ba585e49feba65da6ca1","affectsGlobalScope":true,"impliedFormat":1},{"version":"21d819c173c0cf7cc3ce57c3276e77fd9a8a01d35a06ad87158781515c9a438a","impliedFormat":1},{"version":"98cffbf06d6bab333473c70a893770dbe990783904002c4f1a960447b4b53dca","affectsGlobalScope":true,"impliedFormat":1},{"version":"3af97acf03cc97de58a3a4bc91f8f616408099bc4233f6d0852e72a8ffb91ac9","affectsGlobalScope":true,"impliedFormat":1},{"version":"808069bba06b6768b62fd22429b53362e7af342da4a236ed2d2e1c89fcca3b4a","affectsGlobalScope":true,"impliedFormat":1},{"version":"1db0b7dca579049ca4193d034d835f6bfe73096c73663e5ef9a0b5779939f3d0","affectsGlobalScope":true,"impliedFormat":1},{"version":"9798340ffb0d067d69b1ae5b32faa17ab31b82466a3fc00d8f2f2df0c8554aaa","affectsGlobalScope":true,"impliedFormat":1},{"version":"f26b11d8d8e4b8028f1c7d618b22274c892e4b0ef5b3678a8ccbad85419aef43","affectsGlobalScope":true,"impliedFormat":1},{"version":"5929864ce17fba74232584d90cb721a89b7ad277220627cc97054ba15a98ea8f","impliedFormat":1},{"version":"763fe0f42b3d79b440a9b6e51e9ba3f3f91352469c1e4b3b67bfa4ff6352f3f4","impliedFormat":1},{"version":"25c8056edf4314820382a5fdb4bb7816999acdcb929c8f75e3f39473b87e85bc","impliedFormat":1},{"version":"c464d66b20788266e5353b48dc4aa6bc0dc4a707276df1e7152ab0c9ae21fad8","impliedFormat":1},{"version":"78d0d27c130d35c60b5e5566c9f1e5be77caf39804636bc1a40133919a949f21","impliedFormat":1},{"version":"c6fd2c5a395f2432786c9cb8deb870b9b0e8ff7e22c029954fabdd692bff6195","impliedFormat":1},{"version":"1d6e127068ea8e104a912e42fc0a110e2aa5a66a356a917a163e8cf9a65e4a75","impliedFormat":1},{"version":"5ded6427296cdf3b9542de4471d2aa8d3983671d4cac0f4bf9c637208d1ced43","impliedFormat":1},{"version":"7f182617db458e98fc18dfb272d40aa2fff3a353c44a89b2c0ccb3937709bfb5","impliedFormat":1},{"version":"cadc8aced301244057c4e7e73fbcae534b0f5b12a37b150d80e5a45aa4bebcbd","impliedFormat":1},{"version":"385aab901643aa54e1c36f5ef3107913b10d1b5bb8cbcd933d4263b80a0d7f20","impliedFormat":1},{"version":"9670d44354bab9d9982eca21945686b5c24a3f893db73c0dae0fd74217a4c219","impliedFormat":1},{"version":"0b8a9268adaf4da35e7fa830c8981cfa22adbbe5b3f6f5ab91f6658899e657a7","impliedFormat":1},{"version":"11396ed8a44c02ab9798b7dca436009f866e8dae3c9c25e8c1fbc396880bf1bb","impliedFormat":1},{"version":"ba7bc87d01492633cb5a0e5da8a4a42a1c86270e7b3d2dea5d156828a84e4882","impliedFormat":1},{"version":"4893a895ea92c85345017a04ed427cbd6a1710453338df26881a6019432febdd","impliedFormat":1},{"version":"c21dc52e277bcfc75fac0436ccb75c204f9e1b3fa5e12729670910639f27343e","impliedFormat":1},{"version":"13f6f39e12b1518c6650bbb220c8985999020fe0f21d818e28f512b7771d00f9","impliedFormat":1},{"version":"9b5369969f6e7175740bf51223112ff209f94ba43ecd3bb09eefff9fd675624a","impliedFormat":1},{"version":"4fe9e626e7164748e8769bbf74b538e09607f07ed17c2f20af8d680ee49fc1da","impliedFormat":1},{"version":"24515859bc0b836719105bb6cc3d68255042a9f02a6022b3187948b204946bd2","impliedFormat":1},{"version":"ea0148f897b45a76544ae179784c95af1bd6721b8610af9ffa467a518a086a43","impliedFormat":1},{"version":"24c6a117721e606c9984335f71711877293a9651e44f59f3d21c1ea0856f9cc9","impliedFormat":1},{"version":"dd3273ead9fbde62a72949c97dbec2247ea08e0c6952e701a483d74ef92d6a17","impliedFormat":1},{"version":"405822be75ad3e4d162e07439bac80c6bcc6dbae1929e179cf467ec0b9ee4e2e","impliedFormat":1},{"version":"0db18c6e78ea846316c012478888f33c11ffadab9efd1cc8bcc12daded7a60b6","impliedFormat":1},{"version":"e61be3f894b41b7baa1fbd6a66893f2579bfad01d208b4ff61daef21493ef0a8","impliedFormat":1},{"version":"bd0532fd6556073727d28da0edfd1736417a3f9f394877b6d5ef6ad88fba1d1a","impliedFormat":1},{"version":"89167d696a849fce5ca508032aabfe901c0868f833a8625d5a9c6e861ef935d2","impliedFormat":1},{"version":"615ba88d0128ed16bf83ef8ccbb6aff05c3ee2db1cc0f89ab50a4939bfc1943f","impliedFormat":1},{"version":"a4d551dbf8746780194d550c88f26cf937caf8d56f102969a110cfaed4b06656","impliedFormat":1},{"version":"8bd86b8e8f6a6aa6c49b71e14c4ffe1211a0e97c80f08d2c8cc98838006e4b88","impliedFormat":1},{"version":"317e63deeb21ac07f3992f5b50cdca8338f10acd4fbb7257ebf56735bf52ab00","impliedFormat":1},{"version":"4732aec92b20fb28c5fe9ad99521fb59974289ed1e45aecb282616202184064f","impliedFormat":1},{"version":"2e85db9e6fd73cfa3d7f28e0ab6b55417ea18931423bd47b409a96e4a169e8e6","impliedFormat":1},{"version":"c46e079fe54c76f95c67fb89081b3e399da2c7d109e7dca8e4b58d83e332e605","impliedFormat":1},{"version":"bf67d53d168abc1298888693338cb82854bdb2e69ef83f8a0092093c2d562107","impliedFormat":1},{"version":"b52476feb4a0cbcb25e5931b930fc73cb6643fb1a5060bf8a3dda0eeae5b4b68","affectsGlobalScope":true,"impliedFormat":1},{"version":"f9501cc13ce624c72b61f12b3963e84fad210fbdf0ffbc4590e08460a3f04eba","affectsGlobalScope":true,"impliedFormat":1},{"version":"e7721c4f69f93c91360c26a0a84ee885997d748237ef78ef665b153e622b36c1","affectsGlobalScope":true,"impliedFormat":1},{"version":"0fa06ada475b910e2106c98c68b10483dc8811d0c14a8a8dd36efb2672485b29","impliedFormat":1},{"version":"33e5e9aba62c3193d10d1d33ae1fa75c46a1171cf76fef750777377d53b0303f","impliedFormat":1},{"version":"2b06b93fd01bcd49d1a6bd1f9b65ddcae6480b9a86e9061634d6f8e354c1468f","impliedFormat":1},{"version":"6a0cd27e5dc2cfbe039e731cf879d12b0e2dded06d1b1dedad07f7712de0d7f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"13f5c844119c43e51ce777c509267f14d6aaf31eafb2c2b002ca35584cd13b29","impliedFormat":1},{"version":"e60477649d6ad21542bd2dc7e3d9ff6853d0797ba9f689ba2f6653818999c264","impliedFormat":1},{"version":"c2510f124c0293ab80b1777c44d80f812b75612f297b9857406468c0f4dafe29","affectsGlobalScope":true,"impliedFormat":1},{"version":"5524481e56c48ff486f42926778c0a3cce1cc85dc46683b92b1271865bcf015a","impliedFormat":1},{"version":"4c829ab315f57c5442c6667b53769975acbf92003a66aef19bce151987675bd1","affectsGlobalScope":true,"impliedFormat":1},{"version":"b2ade7657e2db96d18315694789eff2ddd3d8aea7215b181f8a0b303277cc579","impliedFormat":1},{"version":"9855e02d837744303391e5623a531734443a5f8e6e8755e018c41d63ad797db2","impliedFormat":1},{"version":"4d631b81fa2f07a0e63a9a143d6a82c25c5f051298651a9b69176ba28930756d","impliedFormat":1},{"version":"836a356aae992ff3c28a0212e3eabcb76dd4b0cc06bcb9607aeef560661b860d","impliedFormat":1},{"version":"1e0d1f8b0adfa0b0330e028c7941b5a98c08b600efe7f14d2d2a00854fb2f393","impliedFormat":1},{"version":"41670ee38943d9cbb4924e436f56fc19ee94232bc96108562de1a734af20dc2c","affectsGlobalScope":true,"impliedFormat":1},{"version":"c906fb15bd2aabc9ed1e3f44eb6a8661199d6c320b3aa196b826121552cb3695","impliedFormat":1},{"version":"22295e8103f1d6d8ea4b5d6211e43421fe4564e34d0dd8e09e520e452d89e659","impliedFormat":1},{"version":"58647d85d0f722a1ce9de50955df60a7489f0593bf1a7015521efe901c06d770","impliedFormat":1},{"version":"9018e9493596e2d041fc531f5101a0d05f9d2e1311f4ae3c74345b1766158c08","impliedFormat":1},{"version":"a10f0e1854f3316d7ee437b79649e5a6ae3ae14ffe6322b02d4987071a95362e","impliedFormat":1},{"version":"e208f73ef6a980104304b0d2ca5f6bf1b85de6009d2c7e404028b875020fa8f2","impliedFormat":1},{"version":"d163b6bc2372b4f07260747cbc6c0a6405ab3fbcea3852305e98ac43ca59f5bc","impliedFormat":1},{"version":"e6fa9ad47c5f71ff733744a029d1dc472c618de53804eae08ffc243b936f87ff","affectsGlobalScope":true,"impliedFormat":1},{"version":"a6f137d651076822d4fe884287e68fd61785a0d3d1fdb250a5059b691fa897db","impliedFormat":1},{"version":"24826ed94a78d5c64bd857570fdbd96229ad41b5cb654c08d75a9845e3ab7dde","impliedFormat":1},{"version":"8b479a130ccb62e98f11f136d3ac80f2984fdc07616516d29881f3061f2dd472","impliedFormat":1},{"version":"928af3d90454bf656a52a48679f199f64c1435247d6189d1caf4c68f2eaf921f","affectsGlobalScope":true,"impliedFormat":1},{"version":"bceb58df66ab8fb00170df20cd813978c5ab84be1d285710c4eb005d8e9d8efb","affectsGlobalScope":true,"impliedFormat":1},{"version":"3f16a7e4deafa527ed9995a772bb380eb7d3c2c0fd4ae178c5263ed18394db2c","impliedFormat":1},{"version":"933921f0bb0ec12ef45d1062a1fc0f27635318f4d294e4d99de9a5493e618ca2","impliedFormat":1},{"version":"71a0f3ad612c123b57239a7749770017ecfe6b66411488000aba83e4546fde25","impliedFormat":1},{"version":"77fbe5eecb6fac4b6242bbf6eebfc43e98ce5ccba8fa44e0ef6a95c945ff4d98","impliedFormat":1},{"version":"4f9d8ca0c417b67b69eeb54c7ca1bedd7b56034bb9bfd27c5d4f3bc4692daca7","impliedFormat":1},{"version":"814118df420c4e38fe5ae1b9a3bafb6e9c2aa40838e528cde908381867be6466","impliedFormat":1},{"version":"a3fc63c0d7b031693f665f5494412ba4b551fe644ededccc0ab5922401079c95","impliedFormat":1},{"version":"80523c00b8544a2000ae0143e4a90a00b47f99823eb7926c1e03c494216fc363","impliedFormat":1},{"version":"37ba7b45141a45ce6e80e66f2a96c8a5ab1bcef0fc2d0f56bb58df96ec67e972","impliedFormat":1},{"version":"45650f47bfb376c8a8ed39d4bcda5902ab899a3150029684ee4c10676d9fbaee","impliedFormat":1},{"version":"746911b62b329587939560deb5c036aca48aece03147b021fa680223255d5183","affectsGlobalScope":true,"impliedFormat":1},{"version":"18fd40412d102c5564136f29735e5d1c3b455b8a37f920da79561f1fde068208","impliedFormat":1},{"version":"c8d3e5a18ba35629954e48c4cc8f11dc88224650067a172685c736b27a34a4dc","impliedFormat":1},{"version":"f0be1b8078cd549d91f37c30c222c2a187ac1cf981d994fb476a1adc61387b14","affectsGlobalScope":true,"impliedFormat":1},{"version":"0aaed1d72199b01234152f7a60046bc947f1f37d78d182e9ae09c4289e06a592","impliedFormat":1},{"version":"2b55d426ff2b9087485e52ac4bc7cfafe1dc420fc76dad926cd46526567c501a","impliedFormat":1},{"version":"66ba1b2c3e3a3644a1011cd530fb444a96b1b2dfe2f5e837a002d41a1a799e60","impliedFormat":1},{"version":"7e514f5b852fdbc166b539fdd1f4e9114f29911592a5eb10a94bb3a13ccac3c4","impliedFormat":1},{"version":"5b7aa3c4c1a5d81b411e8cb302b45507fea9358d3569196b27eb1a27ae3a90ef","affectsGlobalScope":true,"impliedFormat":1},{"version":"5987a903da92c7462e0b35704ce7da94d7fdc4b89a984871c0e2b87a8aae9e69","affectsGlobalScope":true,"impliedFormat":1},{"version":"ea08a0345023ade2b47fbff5a76d0d0ed8bff10bc9d22b83f40858a8e941501c","impliedFormat":1},{"version":"47613031a5a31510831304405af561b0ffaedb734437c595256bb61a90f9311b","impliedFormat":1},{"version":"ae062ce7d9510060c5d7e7952ae379224fb3f8f2dd74e88959878af2057c143b","impliedFormat":1},{"version":"8a1a0d0a4a06a8d278947fcb66bf684f117bf147f89b06e50662d79a53be3e9f","affectsGlobalScope":true,"impliedFormat":1},{"version":"358765d5ea8afd285d4fd1532e78b88273f18cb3f87403a9b16fef61ac9fdcfe","impliedFormat":1},{"version":"9f55299850d4f0921e79b6bf344b47c420ce0f507b9dcf593e532b09ea7eeea1","impliedFormat":1},{"version":"865a2612f5ec073dd48d454307ccabb04c48f8b96fda9940c5ebfe6b4b451f51","impliedFormat":1},{"version":"89e7fd23f6e6ced38596054161f5fb88737018909c6529c946cb349b74b95275","impliedFormat":1},{"version":"115b2ad73fa7d175cd71a5873d984c21593b2a022f1a2036cc39d9f53629e5dc","impliedFormat":1},{"version":"1be330b3a0b00590633f04c3b35db7fa618c9ee079258e2b24c137eb4ffcd728","impliedFormat":1},{"version":"bb7a61dd55dc4b9422d13da3a6bb9cc5e89be888ef23bbcf6558aa9726b89a1c","impliedFormat":1},{"version":"3da0083607976261730c44908eab1b6262f727747ef3230a65ecd0153d9e8639","impliedFormat":1},{"version":"db6d2d9daad8a6d83f281af12ce4355a20b9a3e71b82b9f57cddcca0a8964a96","impliedFormat":1},{"version":"dd721e5707f241e4ef4ab36570d9e2a79f66aad63a339e3cbdbac7d9164d2431","impliedFormat":1},{"version":"24f8562308dd8ba6013120557fa7b44950b619610b2c6cb8784c79f11e3c4f90","impliedFormat":1},{"version":"3849a7f92d0e11b785f6ae7bedb25d9aad8d1234b3f1cf530a4e7404be26dd0a","impliedFormat":1},{"version":"a86f82d646a739041d6702101afa82dcb935c416dd93cbca7fd754fd0282ce1f","impliedFormat":1},{"version":"57d6ac03382e30e9213641ff4f18cf9402bb246b77c13c8e848c0b1ca2b7ef92","impliedFormat":1},{"version":"f040772329d757ecd38479991101ef7bc9bf8d8f4dd8ee5d96fe00aa264f2a2b","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"57e47d02e88abef89d214cdf52b478104dc17997015746e288cbb580beaef266","impliedFormat":1},{"version":"04a2d0bd8166f057cc980608bd5898bfc91198636af3c1eb6cb4eb5e8652fbea","impliedFormat":1},{"version":"376c21ad92ca004531807ea4498f90a740fd04598b45a19335a865408180eddd","impliedFormat":1},{"version":"9e2739b32f741859263fdba0244c194ca8e96da49b430377930b8f721d77c000","impliedFormat":1},{"version":"a9af0e608929aaf9ce96bd7a7b99c9360636c31d73670e4af09a09950df97841","impliedFormat":1},{"version":"48d37b90a04e753a925228f50304d02c4f95d57bf682f8bb688621c3cd9d32ec","impliedFormat":1},{"version":"361e2b13c6765d7f85bb7600b48fde782b90c7c41105b7dab1f6e7871071ba20","impliedFormat":1},{"version":"c86fe861cf1b4c46a0fb7d74dffe596cf679a2e5e8b1456881313170f092e3fa","impliedFormat":1},{"version":"b6db56e4903e9c32e533b78ac85522de734b3d3a8541bf24d256058d464bf04b","impliedFormat":1},{"version":"24daa0366f837d22c94a5c0bad5bf1fd0f6b29e1fae92dc47c3072c3fdb2fbd5","impliedFormat":1},{"version":"b68c4ed987ef5693d3dccd85222d60769463aca404f2ffca1c4c42781dce388e","impliedFormat":1},{"version":"cfb5b5d514eb4ad0ee25f313b197f3baa493eee31f27613facd71efb68206720","impliedFormat":1},{"version":"65f43099ded6073336e697512d9b80f2d4fec3182b7b2316abf712e84104db00","impliedFormat":1},{"version":"3e7efde639c6a6c3edb9847b3f61e308bf7a69685b92f665048c45132f51c218","impliedFormat":1},{"version":"df45ca1176e6ac211eae7ddf51336dc075c5314bc5c253651bae639defd5eec5","impliedFormat":1},{"version":"106c6025f1d99fd468fd8bf6e5bda724e11e5905a4076c5d29790b6c3745e50c","impliedFormat":1},{"version":"9715fe982fccf375c88ac4d3cc8f6a126a7b7596be8d60190a0c7d22b45b4be4","impliedFormat":1},{"version":"1fe24e25a00c7dd689cb8c0fb4f1048b4a6d1c50f76aaca2ca5c6cdb44e01442","impliedFormat":1},{"version":"672f293c53a07b8c1c1940797cd5c7984482a0df3dd9c1f14aaee8d3474c2d83","impliedFormat":1},{"version":"0a66cb2511fa8e3e0e6ba9c09923f664a0a00896f486e6f09fc11ff806a12b0c","impliedFormat":1},{"version":"d703f98676a44f90d63b3ffc791faac42c2af0dd2b4a312f4afdb5db471df3de","impliedFormat":1},{"version":"0cfe1d0b90d24f5c105db5a2117192d082f7d048801d22a9ea5c62fae07b80a0","impliedFormat":1},{"version":"ef61792acbfa8c27c9bd113f02731e66229f7d3a169e3c1993b508134f1a58e0","impliedFormat":1},{"version":"9c82171d836c47486074e4ca8e059735bf97b205e70b196535b5efd40cbe1bc5","impliedFormat":1},{"version":"414cc05e215b7fc5a4a6ece431985e05e03762c8eb5bf1e0972d477f97832956","impliedFormat":1},{"version":"c5426dbfc1cf90532f66965a7aa8c1136a78d4d0f96d8180ecbfc11d7722f1a5","impliedFormat":1},{"version":"5c2e5ca7d53236bbf483a81ae283e2695e291fe69490cd139b33fa9e71838a69","impliedFormat":1},{"version":"a73bee51e3820392023252c36348e62dd72e6bae30a345166e9c78360f1aba7e","impliedFormat":1},{"version":"6ea68b3b7d342d1716cc4293813410d3f09ff1d1ca4be14c42e6d51e810962e1","impliedFormat":1},{"version":"c319e82ac16a5a5da9e28dfdefdad72cebb5e1e67cbdcc63cce8ae86be1e454f","impliedFormat":1},{"version":"a23185bc5ef590c287c28a91baf280367b50ae4ea40327366ad01f6f4a8edbc5","impliedFormat":1},{"version":"65a15fc47900787c0bd18b603afb98d33ede930bed1798fc984d5ebb78b26cf9","impliedFormat":1},{"version":"9d202701f6e0744adb6314d03d2eb8fc994798fc83d91b691b75b07626a69801","impliedFormat":1},{"version":"de9d2df7663e64e3a91bf495f315a7577e23ba088f2949d5ce9ec96f44fba37d","impliedFormat":1},{"version":"c7af78a2ea7cb1cd009cfb5bdb48cd0b03dad3b54f6da7aab615c2e9e9d570c5","impliedFormat":1},{"version":"1ee45496b5f8bdee6f7abc233355898e5bf9bd51255db65f5ff7ede617ca0027","impliedFormat":1},{"version":"0c7c947ff881c4274c0800deaa0086971e0bfe51f89a33bd3048eaa3792d4876","affectsGlobalScope":true,"impliedFormat":1},{"version":"db01d18853469bcb5601b9fc9826931cc84cc1a1944b33cad76fd6f1e3d8c544","affectsGlobalScope":true,"impliedFormat":1},{"version":"dba114fb6a32b355a9cfc26ca2276834d72fe0e94cd2c3494005547025015369","impliedFormat":1},{"version":"a020158a317c07774393974d26723af551e569f1ba4d6524e8e245f10e11b976","affectsGlobalScope":true,"impliedFormat":1},{"version":"fa6c12a7c0f6b84d512f200690bfc74819e99efae69e4c95c4cd30f6884c526e","impliedFormat":1},{"version":"f1c32f9ce9c497da4dc215c3bc84b722ea02497d35f9134db3bb40a8d918b92b","impliedFormat":1},{"version":"b73c319af2cc3ef8f6421308a250f328836531ea3761823b4cabbd133047aefa","affectsGlobalScope":true,"impliedFormat":1},{"version":"e433b0337b8106909e7953015e8fa3f2d30797cea27141d1c5b135365bb975a6","impliedFormat":1},{"version":"15b36126e0089bfef173ab61329e8286ce74af5e809d8a72edcafd0cc049057f","impliedFormat":1},{"version":"ddff7fc6edbdc5163a09e22bf8df7bef75f75369ebd7ecea95ba55c4386e2441","impliedFormat":1},{"version":"13283350547389802aa35d9f2188effaeac805499169a06ef5cd77ce2a0bd63f","impliedFormat":1},{"version":"2e4f37ffe8862b14d8e24ae8763daaa8340c0df0b859d9a9733def0eee7562d9","impliedFormat":1},{"version":"d07cbc787a997d83f7bde3877fec5fb5b12ce8c1b7047eb792996ed9726b4dde","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"8bba776476c48b0e319d243f353190f24096057acede3c2f620fee17ff885dba","impliedFormat":1},{"version":"a3abe92070fbd33714bd837806030b39cfb1f8283a98c7c1f55fffeea388809e","impliedFormat":1},{"version":"ceb6696b98a72f2dae802260c5b0940ea338de65edd372ff9e13ab0a410c3a88","impliedFormat":1},{"version":"2cd914e04d403bdc7263074c63168335d44ce9367e8a74f6896c77d4d26a1038","impliedFormat":1},{"version":"ac60bbee0d4235643cc52b57768b22de8c257c12bd8c2039860540cab1fa1d82","impliedFormat":1},{"version":"b73cbf0a72c8800cf8f96a9acfe94f3ad32ca71342a8908b8ae484d61113f647","impliedFormat":1},{"version":"bae6dd176832f6423966647382c0d7ba9e63f8c167522f09a982f086cd4e8b23","impliedFormat":1},{"version":"208c9af9429dd3c76f5927b971263174aaa4bc7621ddec63f163640cbd3c473c","impliedFormat":1},{"version":"20865ac316b8893c1a0cc383ccfc1801443fbcc2a7255be166cf90d03fac88c9","impliedFormat":1},{"version":"c9958eb32126a3843deedda8c22fb97024aa5d6dd588b90af2d7f2bfac540f23","impliedFormat":1},{"version":"3bc8605900fd1668f6d93ce8e14386478b6caa6fda41be633ee0fe4d0c716e62","impliedFormat":1},{"version":"461d0ad8ae5f2ff981778af912ba71b37a8426a33301daa00f21c6ccb27f8156","impliedFormat":1},{"version":"e927c2c13c4eaf0a7f17e6022eee8519eb29ef42c4c13a31e81a611ab8c95577","impliedFormat":1},{"version":"fcafff163ca5e66d3b87126e756e1b6dfa8c526aa9cd2a2b0a9da837d81bbd72","impliedFormat":1},{"version":"70246ad95ad8a22bdfe806cb5d383a26c0c6e58e7207ab9c431f1cb175aca657","impliedFormat":1},{"version":"f00f3aa5d64ff46e600648b55a79dcd1333458f7a10da2ed594d9f0a44b76d0b","impliedFormat":1},{"version":"772d8d5eb158b6c92412c03228bd9902ccb1457d7a705b8129814a5d1a6308fc","impliedFormat":1},{"version":"45490817629431853543adcb91c0673c25af52a456479588b6486daba34f68bb","impliedFormat":1},{"version":"802e797bcab5663b2c9f63f51bdf67eff7c41bc64c0fd65e6da3e7941359e2f7","impliedFormat":1},{"version":"b01bd582a6e41457bc56e6f0f9de4cb17f33f5f3843a7cf8210ac9c18472fb0f","impliedFormat":1},{"version":"9f31420a5040dbfb49ab94bcaaa5103a9a464e607cabe288958f53303f1da32e","impliedFormat":1},{"version":"6124e973eab8c52cabf3c07575204efc1784aca6b0a30c79eb85fe240a857efa","impliedFormat":1},{"version":"0d891735a21edc75df51f3eb995e18149e119d1ce22fd40db2b260c5960b914e","impliedFormat":1},{"version":"3b414b99a73171e1c4b7b7714e26b87d6c5cb03d200352da5342ab4088a54c85","impliedFormat":1},{"version":"f11d0dcaa4a1cba6d6513b04ceb31a262f223f56e18b289c0ba3133b4d3cd9a6","impliedFormat":1},{"version":"0a437ae178f999b46b6153d79095b60c42c996bc0458c04955f1c996dc68b971","impliedFormat":1},{"version":"74b2a5e5197bd0f2e0077a1ea7c07455bbea67b87b0869d9786d55104006784f","impliedFormat":1},{"version":"4a7baeb6325920044f66c0f8e5e6f1f52e06e6d87588d837bdf44feb6f35c664","impliedFormat":1},{"version":"6dcf60530c25194a9ee0962230e874ff29d34c59605d8e069a49928759a17e0a","impliedFormat":1},{"version":"56013416784a6b754f3855f8f2bf6ce132320679b8a435389aca0361bce4df6b","impliedFormat":1},{"version":"43e96a3d5d1411ab40ba2f61d6a3192e58177bcf3b133a80ad2a16591611726d","impliedFormat":1},{"version":"9c066f3b46cf016e5d072b464821c5b21cc9adcc44743de0f6c75e2509a357ab","impliedFormat":1},{"version":"002eae065e6960458bda3cf695e578b0d1e2785523476f8a9170b103c709cd4f","impliedFormat":1},{"version":"c51641ab4bfa31b7a50a0ca37edff67f56fab3149881024345b13f2b48b7d2de","impliedFormat":1},{"version":"a57b1802794433adec9ff3fed12aa79d671faed86c49b09e02e1ac41b4f1d33a","impliedFormat":1},{"version":"b620391fe8060cf9bedc176a4d01366e6574d7a71e0ac0ab344a4e76576fcbb8","impliedFormat":1},{"version":"52abbd5035a97ebfb4240ec8ade2741229a7c26450c84eb73490dc5ea048b911","impliedFormat":1},{"version":"1042064ece5bb47d6aba91648fbe0635c17c600ebdf567588b4ca715602f0a9d","impliedFormat":1},{"version":"4360ad4de54de2d5c642c4375d5eab0e7fe94ebe8adca907e6c186bbef75a54d","impliedFormat":1},{"version":"c338dff3233675f87a3869417aaea8b8bf590505106d38907dc1d0144f6402ef","impliedFormat":1},{"version":"7bb79aa2fead87d9d56294ef71e056487e848d7b550c9a367523ee5416c44cfa","impliedFormat":1},{"version":"9c9cae45dc94c2192c7d25f80649414fa13c425d0399a2c7cb2b979e4e50af42","impliedFormat":1},{"version":"068f063c2420b20f8845afadb38a14c640aed6bb01063df224edb24af92b4550","impliedFormat":1},{"version":"27ff4196654e6373c9af16b6165120e2dd2169f9ad6abb5c935af5abd8c7938c","impliedFormat":1},{"version":"b8719d4483ebef35e9cb67cd5677b7e0103cf2ed8973df6aba6fdd02896ddc6e","impliedFormat":1},{"version":"643672ce383e1c58ea665a92c5481f8441edbd3e91db36e535abccbc9035adeb","impliedFormat":1},{"version":"6dd9bcf10678b889842d467706836a0ab42e6c58711e33918ed127073807ee65","impliedFormat":1},{"version":"8fa022ea514ce0ea78ac9b7092a9f97f08ead20c839c779891019e110fce8307","impliedFormat":1},{"version":"c93235337600b786fd7d0ff9c71a00f37ca65c4d63e5d695fc75153be2690f09","impliedFormat":1},{"version":"10179c817a384983f6925f778a2dac2c9427817f7d79e27d3e9b1c8d0564f1f4","impliedFormat":1},{"version":"ce791f6ea807560f08065d1af6014581eeb54a05abd73294777a281b6dfd73c2","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"42c169fb8c2d42f4f668c624a9a11e719d5d07dacbebb63cbcf7ef365b0a75b3","impliedFormat":1},{"version":"c0a666b005521f52e2db0b685d659d7ee9b0b60bc0d347dfc5e826c7957bdb83","impliedFormat":1},{"version":"807d38d00ce6ab9395380c0f64e52f2f158cc804ac22745d8f05f0efdec87c33","impliedFormat":1},{"version":"ce0df82a9ae6f914ba08409d4d883983cc08e6d59eb2df02d8e4d68309e7848b","impliedFormat":1},{"version":"796273b2edc72e78a04e86d7c58ae94d370ab93a0ddf40b1aa85a37a1c29ecd7","impliedFormat":1},{"version":"5df15a69187d737d6d8d066e189ae4f97e41f4d53712a46b2710ff9f8563ec9f","impliedFormat":1},{"version":"e17cd049a1448de4944800399daa4a64c5db8657cc9be7ef46be66e2a2cd0e7c","impliedFormat":1},{"version":"d05fb434f4ba073aed74b6c62eff1723c835de2a963dbb091e000a2decb5a691","impliedFormat":1},{"version":"10e6166be454ddb8c81000019ce1069b476b478c316e7c25965a91904ec5c1e3","impliedFormat":1},{"version":"43ba4f2fa8c698f5c304d21a3ef596741e8e85a810b7c1f9b692653791d8d97a","impliedFormat":1},{"version":"4d4927cbee21750904af7acf940c5e3c491b4d5ebc676530211e389dd375607a","impliedFormat":1},{"version":"72105519d0390262cf0abe84cf41c926ade0ff475d35eb21307b2f94de985778","impliedFormat":1},{"version":"703989a003790524b4e34a1758941d05c121d5d352bccca55a5cfb0c76bca592","impliedFormat":1},{"version":"a58abf1f5c8feb335475097abeddd32fd71c4dc2065a3d28cf15cacabad9654a","impliedFormat":1},{"version":"ccf6dd45b708fb74ba9ed0f2478d4eb9195c9dfef0ff83a6092fa3cf2ff53b4f","impliedFormat":1},{"version":"2d7db1d73456e8c5075387d4240c29a2a900847f9c1bff106a2e490da8fbd457","impliedFormat":1},{"version":"2b15c805f48e4e970f8ec0b1915f22d13ca6212375e8987663e2ef5f0205e832","impliedFormat":1},{"version":"671aeae7130038566a8d00affeb1b3e3b131edf93cbcfff6f55ed68f1ca4c1b3","impliedFormat":1},{"version":"f0f05149debcf31b3a717ce8dd16e0323a789905cb9e27239167b604153b8885","impliedFormat":1},{"version":"35069c2c417bd7443ae7c7cafd1de02f665bf015479fec998985ffbbf500628c","impliedFormat":1},{"version":"955c69dde189d5f47a886ed454ff50c69d4d8aaec3a454c9ab9c3551db727861","impliedFormat":1},{"version":"cec8b16ff98600e4f6777d1e1d4ddf815a5556a9c59bc08cc16db4fd4ae2cf00","impliedFormat":1},{"version":"9e21f8e2c0cfea713a4a372f284b60089c0841eb90bf3610539d89dbcd12d65a","impliedFormat":1},{"version":"045b752f44bf9bbdcaffd882424ab0e15cb8d11fa94e1448942e338c8ef19fba","impliedFormat":1},{"version":"2894c56cad581928bb37607810af011764a2f511f575d28c9f4af0f2ef02d1ab","impliedFormat":1},{"version":"0a72186f94215d020cb386f7dca81d7495ab6c17066eb07d0f44a5bf33c1b21a","impliedFormat":1},{"version":"c226288bda11cee97850f0149cc4ff5a244d42ed3f5a9f6e9b02f1162bf1e3f4","impliedFormat":1},{"version":"210a4ec6fd58f6c0358e68f69501a74aef547c82deb920c1dec7fa04f737915a","impliedFormat":1},{"version":"8eea4cc42d04d26bcbcaf209366956e9f7abaf56b0601c101016bb773730c5fe","impliedFormat":1},{"version":"f5319e38724c54dff74ee734950926a745c203dcce00bb0343cb08fbb2f6b546","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"12b8dfed70961bea1861e5d39e433580e71323abb5d33da6605182ec569db584","impliedFormat":1},{"version":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881","impliedFormat":1},{"version":"7e560f533aaf88cf9d3b427dcf6c112dd3f2ee26d610e2587583b6c354c753db","impliedFormat":1},{"version":"71e0082342008e4dfb43202df85ea0986ef8e003c921a1e49999d0234a3019da","impliedFormat":1},{"version":"27ab780875bcbb65e09da7496f2ca36288b0c541abaa75c311450a077d54ec15","impliedFormat":1},{"version":"2652448ac55a2010a1f71dd141f828b682298d39728f9871e1cdf8696ef443fd","impliedFormat":1},{"version":"e71e103fb212e015394def7f1379706fce637fec9f91aa88410a73b7c5cbd4e3","impliedFormat":1},{"version":"120599fd965257b1f4d0ff794bc696162832d9d8467224f4665f713a3119078b","impliedFormat":1},{"version":"5433f33b0a20300cca35d2f229a7fc20b0e8477c44be2affeb21cb464af60c76","impliedFormat":1},{"version":"db036c56f79186da50af66511d37d9fe77fa6793381927292d17f81f787bb195","impliedFormat":1},{"version":"bd4131091b773973ca5d2326c60b789ab1f5e02d8843b3587effe6e1ea7c9d86","impliedFormat":1},{"version":"794998dc1c5a19ce77a75086fe829fb9c92f2fd07b5631c7d5e0d04fd9bc540c","impliedFormat":1},{"version":"2b0b12d0ee52373b1e7b09226eae8fbf6a2043916b7c19e2c39b15243f32bde2","impliedFormat":1},{"version":"6ac6715916fa75a1f7ebdfeacac09513b4d904b667d827b7535e84ff59679aff","impliedFormat":1},{"version":"0427df5c06fafc5fe126d14b9becd24160a288deff40e838bfbd92a35f8d0d00","impliedFormat":1},{"version":"bdc5fd605a6d315ded648abf2c691a22d0b0c774b78c15512c40ddf138e51950","impliedFormat":1},{"version":"49c346823ba6d4b12278c12c977fb3a31c06b9ca719015978cb145eb86da1c61","impliedFormat":1},{"version":"bfac6e50eaa7e73bb66b7e052c38fdc8ccfc8dbde2777648642af33cf349f7f1","impliedFormat":1},{"version":"92f7c1a4da7fbfd67a2228d1687d5c2e1faa0ba865a94d3550a3941d7527a45d","impliedFormat":1},{"version":"f53b120213a9289d9a26f5af90c4c686dd71d91487a0aa5451a38366c70dc64b","impliedFormat":1},{"version":"6cd4b0986c638d92f7204d1407b1cb3e0a79d7a2d23b0f141c1a0829540ce7ef","impliedFormat":1},{"version":"57d67b72e06059adc5e9454de26bbfe567d412b962a501d263c75c2db430f40e","impliedFormat":1},{"version":"6511e4503cf74c469c60aafd6589e4d14d5eb0a25f9bf043dcbecdf65f261972","impliedFormat":1},{"version":"d58265e159fc3cb30aa8878ba5e986a314b1759c824ff66d777b9fe42117231a","impliedFormat":1},{"version":"ff8fccaae640b0bb364340216dcc7423e55b6bb182ca2334837fee38636ad32e","impliedFormat":1},{"version":"a67b87d0281c97dfc1197ef28dfe397fc2c865ccd41f7e32b53f647184cc7307","impliedFormat":1},{"version":"771ffb773f1ddd562492a6b9aaca648192ac3f056f0e1d997678ff97dbb6bf9b","impliedFormat":1},{"version":"232f70c0cf2b432f3a6e56a8dc3417103eb162292a9fd376d51a3a9ea5fbbf6f","impliedFormat":1},{"version":"59ee66cf96b093b18c90a8f6dbb3f0e3b65c758fba7b8b980af9f2726c32c1a2","impliedFormat":1},{"version":"c590195790d7fa35b4abed577a605d283b8336b9e01fa9bf4ae4be49855940f9","impliedFormat":1},{"version":"8a0e762ceb20c7e72504feef83d709468a70af4abccb304f32d6b9bac1129b2c","impliedFormat":1},{"version":"026a43d8239b8f12d2fc4fa5a7acbc2ad06dd989d8c71286d791d9f57ca22b78","impliedFormat":1},{"version":"9252d498a77517aab5d8d4b5eb9d71e4b225bbc7123df9713e08181de63180f6","impliedFormat":1},{"version":"14cf3683955f914b4695e92c93aae5f3fe1e60f3321d712605164bfe53b34334","impliedFormat":1},{"version":"12f0fb50e28b9d48fe5b7580580efe7cc0bd38e4b8c02d21c175aa9a4fd839b0","impliedFormat":1},{"version":"1fffe726740f9787f15b532e1dc870af3cd964dbe29e191e76121aa3dd8693f2","impliedFormat":1},{"version":"7cd657e359eac7829db5f02c856993e8945ffccc71999cdfb4ab3bf801a1bbc6","impliedFormat":1},{"version":"1a82deef4c1d39f6882f28d275cad4c01f907b9b39be9cbc472fcf2cf051e05b","impliedFormat":1},{"version":"4b20fcf10a5413680e39f5666464859fc56b1003e7dfe2405ced82371ebd49b6","impliedFormat":1},{"version":"29c2aa0712786a4a504fce3acd50928f086027276f7490965cb467d2ce638bae","impliedFormat":1},{"version":"f14e63395b54caecc486f00a39953ab00b7e4d428a4e2c38325154b08eb5dcc2","impliedFormat":1},{"version":"e749bbd37dadf82c9833278780527c717226e1e2c9bc7b2576c8ec1c40ec5647","impliedFormat":1},{"version":"46e4e179b295f08d0bd0176fe44cf6c89558c9091d3cb3894f9eaaa42ea1add1","impliedFormat":1},{"version":"08f52a9edaabeda3b2ea19a54730174861ceed637c5ca1c1b0c39459fdc0853e","impliedFormat":1},{"version":"6459054aabb306821a043e02b89d54da508e3a6966601a41e71c166e4ea1474f","impliedFormat":1},{"version":"bb37588926aba35c9283fe8d46ebf4e79ffe976343105f5c6d45f282793352b2","impliedFormat":1},{"version":"05c97cddbaf99978f83d96de2d8af86aded9332592f08ce4a284d72d0952c391","impliedFormat":1},{"version":"72179f9dd22a86deaad4cc3490eb0fe69ee084d503b686985965654013f1391b","impliedFormat":1},{"version":"2e6114a7dd6feeef85b2c80120fdbfb59a5529c0dcc5bfa8447b6996c97a69f5","impliedFormat":1},{"version":"7b6ff760c8a240b40dab6e4419b989f06a5b782f4710d2967e67c695ef3e93c4","impliedFormat":1},{"version":"29164fb428c851bc35b632761daad3ae075993a0bf9c43e9e3bc6468b32d9aa5","impliedFormat":1},{"version":"3c01539405051bffccacffd617254c8d0f665cdce00ec568c6f66ccb712b734f","impliedFormat":1},{"version":"ef9021bdfe54f4df005d0b81170bd2da9bfd86ef552cde2a049ba85c9649658f","impliedFormat":1},{"version":"17a1a0d1c492d73017c6e9a8feb79e9c8a2d41ef08b0fe51debc093a0b2e9459","impliedFormat":1},{"version":"f974e4a06953682a2c15d5bd5114c0284d5abf8bc0fe4da25cb9159427b70072","impliedFormat":1},{"version":"50256e9c31318487f3752b7ac12ff365c8949953e04568009c8705db802776fb","impliedFormat":1},{"version":"96e1caae9b78cde35c62fee46c1ec9fa5f12c16bc1e2ab08d48e5921e29a6958","impliedFormat":1},{"version":"8de9fe97fa9e00ec00666fa77ab6e91b35d25af8ca75dabcb01e14ad3299b150","impliedFormat":1},{"version":"9e0327857503a958348d9e8e9dd57ed155a1e6ec0071eb5eb946fe06ccdf7680","impliedFormat":1},{"version":"6c800b281b9e89e69165fd11536195488de3ff53004e55905e6c0059a2d8591e","impliedFormat":1},{"version":"01aa917531e116485beca44a14970834687b857757159769c16b228eb1e49c5f","impliedFormat":1},{"version":"397f568f996f8ffcf12d9156342552b0da42f6571eadba6bce61c99e1651977d","impliedFormat":1},{"version":"e2fd426f3cbc5bbff7860378784037c8fa9c1644785eed83c47c902b99b6cda9","impliedFormat":1},{"version":"d663134457d8d669ae0df34eabd57028bddc04fc444c4bc04bc5215afc91e1f4","impliedFormat":1},{"version":"a52674bc98da7979607e0f44d4c015c59c1b1d264c83fc50ec79ff2cfea06723","impliedFormat":1},{"version":"bcca16e60015db8bbf6bd117e88c5f7269337aebb05fc2b0701ae658a458c9c3","impliedFormat":1},{"version":"5e1246644fab20200cdc7c66348f3c861772669e945f2888ef58b461b81e1cd8","impliedFormat":1},{"version":"eb39550e2485298d91099e8ab2a1f7b32777d9a5ba34e9028ea8df2e64891172","impliedFormat":1},{"version":"e108f38a04a607f9386d68a4c6f3fdae1b712960f11f6482c6f1769bab056c2e","impliedFormat":1},{"version":"a3128a84a9568762a2996df79717d92154d18dd894681fc0ab3a098fa7f8ee3b","affectsGlobalScope":true,"impliedFormat":1},{"version":"347791f3792f436950396dd6171d6450234358001ae7c94ca209f1406566ccbf","impliedFormat":1},{"version":"dd80b1e600d00f5c6a6ba23f455b84a7db121219e68f89f10552c54ba46e4dc9","impliedFormat":1},{"version":"714d8ebb298c7acc9bd1f34bd479c57d12b73371078a0c5a1883a68b8f1b9389","impliedFormat":1},{"version":"616775f16134fa9d01fc677ad3f76e68c051a056c22ab552c64cc281a9686790","impliedFormat":1},{"version":"65c24a8baa2cca1de069a0ba9fba82a173690f52d7e2d0f1f7542d59d5eb4db0","impliedFormat":1},{"version":"f9fe6af238339a0e5f7563acee3178f51db37f32a2e7c09f85273098cee7ec49","impliedFormat":1},{"version":"51bf55bb6eb80f11b3aa59fb0a9571565a7ea304a19381f6da5630f4b2e206c4","impliedFormat":1},{"version":"77e71242e71ebf8528c5802993697878f0533db8f2299b4d36aa015bae08a79c","impliedFormat":1},{"version":"98a787be42bd92f8c2a37d7df5f13e5992da0d967fab794adbb7ee18370f9849","impliedFormat":1},{"version":"02f8ef78d46c5b27f108dbb56709daa0aff625c20247abb0e6bb67cd73439f9f","impliedFormat":1},{"version":"b7fff2d004c5879cae335db8f954eb1d61242d9f2d28515e67902032723caeab","impliedFormat":1},{"version":"5f3dc10ae646f375776b4e028d2bed039a93eebbba105694d8b910feebbe8b9c","impliedFormat":1},{"version":"bb0cd7862b72f5eba39909c9889d566e198fcaddf7207c16737d0c2246112678","impliedFormat":1},{"version":"4545c1a1ceca170d5d83452dd7c4994644c35cf676a671412601689d9a62da35","impliedFormat":1},{"version":"6812502cc640de74782ce9121592ae3765deb1c5c8e795b179736b308dd65e90","impliedFormat":1},{"version":"a2d648d333cf67b9aeac5d81a1a379d563a8ffa91ddd61c6179f68de724260ff","impliedFormat":1},{"version":"2b664c3cc544d0e35276e1fb2d4989f7d4b4027ffc64da34ec83a6ccf2e5c528","impliedFormat":1},{"version":"a3f41ed1b4f2fc3049394b945a68ae4fdefd49fa1739c32f149d32c0545d67f5","impliedFormat":1},{"version":"bad68fd0401eb90fe7da408565c8aee9c7a7021c2577aec92fa1382e8876071a","impliedFormat":1},{"version":"47699512e6d8bebf7be488182427189f999affe3addc1c87c882d36b7f2d0b0e","impliedFormat":1},{"version":"fec01479923e169fb52bd4f668dbeef1d7a7ea6e6d491e15617b46f2cacfa37d","impliedFormat":1},{"version":"8a8fb3097ba52f0ae6530ec6ab34e43e316506eb1d9aa29420a4b1e92a81442d","impliedFormat":1},{"version":"44e09c831fefb6fe59b8e65ad8f68a7ecc0e708d152cfcbe7ba6d6080c31c61e","impliedFormat":1},{"version":"1c0a98de1323051010ce5b958ad47bc1c007f7921973123c999300e2b7b0ecc0","impliedFormat":1},{"version":"b10bc147143031b250dc36815fd835543f67278245bf2d0a46dca765f215124e","impliedFormat":1},{"version":"87affad8e2243635d3a191fa72ef896842748d812e973b7510a55c6200b3c2a4","impliedFormat":1},{"version":"ad036a85efcd9e5b4f7dd5c1a7362c8478f9a3b6c3554654ca24a29aa850a9c5","impliedFormat":1},{"version":"fedebeae32c5cdd1a85b4e0504a01996e4a8adf3dfa72876920d3dd6e42978e7","impliedFormat":1},{"version":"1e4c6ac595b6d734c056ac285b9ee50d27a2c7afe7d15bd14ed16210e71593b0","impliedFormat":1},{"version":"cdf21eee8007e339b1b9945abf4a7b44930b1d695cc528459e68a3adc39a622e","impliedFormat":1},{"version":"330896c1a2b9693edd617be24fbf9e5895d6e18c7955d6c08f028f272b37314d","impliedFormat":1},{"version":"1d9c0a9a6df4e8f29dc84c25c5aa0bb1da5456ebede7a03e03df08bb8b27bae6","impliedFormat":1},{"version":"84380af21da938a567c65ef95aefb5354f676368ee1a1cbb4cae81604a4c7d17","impliedFormat":1},{"version":"1af3e1f2a5d1332e136f8b0b95c0e6c0a02aaabd5092b36b64f3042a03debf28","impliedFormat":1},{"version":"3c7b3aecd652169787b3c512d8f274a3511c475f84dcd6cead164e40cad64480","impliedFormat":1},{"version":"9a01f12466488eccd8d9eafc8fecb9926c175a4bf4a8f73a07c3bcf8b3363282","impliedFormat":1},{"version":"b80f624162276f24a4ec78b8e86fbee80ca255938e12f8b58e7a8f1a6937120b","impliedFormat":1},{"version":"1de80059b8078ea5749941c9f863aa970b4735bdbb003be4925c853a8b6b4450","impliedFormat":1},{"version":"1d079c37fa53e3c21ed3fa214a27507bda9991f2a41458705b19ed8c2b61173d","impliedFormat":1},{"version":"5bf5c7a44e779790d1eb54c234b668b15e34affa95e78eada73e5757f61ed76a","impliedFormat":1},{"version":"5835a6e0d7cd2738e56b671af0e561e7c1b4fb77751383672f4b009f4e161d70","impliedFormat":1},{"version":"5c634644d45a1b6bc7b05e71e05e52ec04f3d73d9ac85d5927f647a5f965181a","impliedFormat":1},{"version":"4b7f74b772140395e7af67c4841be1ab867c11b3b82a51b1aeb692822b76c872","impliedFormat":1},{"version":"27be6622e2922a1b412eb057faa854831b95db9db5035c3f6d4b677b902ab3b7","impliedFormat":1},{"version":"b95a6f019095dd1d48fd04965b50dfd63e5743a6e75478343c46d2582a5132bf","impliedFormat":99},{"version":"c2008605e78208cfa9cd70bd29856b72dda7ad89df5dc895920f8e10bcb9cd0a","impliedFormat":99},{"version":"b97cb5616d2ab82a98ec9ada7b9e9cabb1f5da880ec50ea2b8dc5baa4cbf3c16","impliedFormat":99},{"version":"63a7595a5015e65262557f883463f934904959da563b4f788306f699411e9bac","impliedFormat":1},{"version":"4ba137d6553965703b6b55fd2000b4e07ba365f8caeb0359162ad7247f9707a6","impliedFormat":1},{"version":"00b0f43b3770f66aa1e105327980c0ff17a868d0e5d9f5689f15f8d6bf4fb1f4","affectsGlobalScope":true,"impliedFormat":1},{"version":"272a7e7dbe05e8aaba1662ef1a16bbd57975cc352648b24e7a61b7798f3a0ad7","affectsGlobalScope":true,"impliedFormat":1},{"version":"a1219ee18b9282b4c6a31f1f0bcc9255b425e99363268ba6752a932cf76662f0","impliedFormat":1},{"version":"3dc14e1ab45e497e5d5e4295271d54ff689aeae00b4277979fdd10fa563540ae","impliedFormat":1},{"version":"1d63055b690a582006435ddd3aa9c03aac16a696fac77ce2ed808f3e5a06efab","impliedFormat":1},{"version":"b789bf89eb19c777ed1e956dbad0925ca795701552d22e68fd130a032008b9f9","impliedFormat":1},"f2b3bca04d1bfe583daae1e1f798c92ec24bb6693bd88d0a09ba6802dee362a8",{"version":"402e5c534fb2b85fa771170595db3ac0dd532112c8fa44fc23f233bc6967488b","impliedFormat":1},{"version":"52dcc257df5119fb66d864625112ce5033ac51a4c2afe376a0b299d2f7f76e4a","impliedFormat":1},{"version":"e5bab5f871ef708d52d47b3e5d0aa72a08ee7a152f33931d9a60809711a2a9a3","impliedFormat":1},{"version":"e16dc2a81595736024a206c7d5c8a39bfe2e6039208ef29981d0d95434ba8fcf","impliedFormat":1},{"version":"cc4a4903fb698ca1d961d4c10dce658aa3a479faf40509d526f122b044eaf6a4","impliedFormat":1},{"version":"19ee8416e6473ed6c7adb868fa796b5653cf0fa2a337658e677eaa0d134388c3","impliedFormat":1},{"version":"1328ab4e442614b28cdb3d4b414cf68325c0da0dca07287a338d0654b7a00261","impliedFormat":1},{"version":"a039dc21f045919f3cbee2ec13812cc6cc3eebc99dae4be00973230f468d19a6","impliedFormat":1},{"version":"3fbe57af01460e49dcd29df55d6931e1672bc6f1be0fb073d11410bc16f9037d","impliedFormat":1},{"version":"f760be449e8562ec5c09bb5187e8e1eabf3c113c0c58cddda53ef8c69f3e2131","impliedFormat":1},{"version":"44325ed13294fce6ab825b82947bbeed2611db7dad9d9135260192f375e5a189","impliedFormat":1},{"version":"e392e8fb5b514eafc585601c1d781485aa6dd6a320e75daf1064a4c6918a1b45","impliedFormat":1},{"version":"46e4a36e8ddbdfb4e7330e11c81c970dc8b218611df9183d39c41c5f8c653b55","impliedFormat":1},{"version":"370bde134aa8c2abc926d0e99d3a4d5d5dba65c6ee65459137e4f02670cbf841","impliedFormat":1},{"version":"6332f565867cf4a740a70e30f31cefba37ef7cebcf74f22eab8d744fde6d193e","impliedFormat":1},{"version":"2977b7884aedc895a1d0c9c210c7cf3272c29d6959a08a6fa3ff71e0aff08175","impliedFormat":1},{"version":"17f2922d41ddd032830a91371c948cd9ce903b35c95adca72271a54584f19b0b","impliedFormat":1},{"version":"3eed76ede2a1a14d7c9bb0a642041282dcc264811139d3dd275c9fe14efc9840","impliedFormat":1},{"version":"e3cf0611709328b449ec13f8c436712d62003620ce480139fae46ce001c2ee9f","impliedFormat":1},{"version":"8d369483f0c2b9ee388129cfdb6a43bc8112b377e86a41884bd06e19ce04f4c1","impliedFormat":99},{"version":"b558c9a18ea4e6e4157124465c3ef1063e64640da139e67be5edb22f534f2f08","impliedFormat":1},{"version":"01374379f82be05d25c08d2f30779fa4a4c41895a18b93b33f14aeef51768692","impliedFormat":1},{"version":"b0dee183d4e65cf938242efaf3d833c6b645afb35039d058496965014f158141","impliedFormat":1},{"version":"c0bbbf84d3fbd85dd60d040c81e8964cc00e38124a52e9c5dcdedf45fea3f213","impliedFormat":1},"8ab2020159a945e311929abd23acd6b9e2ad9b665bf31a7247f2e12ccad7662d",{"version":"751764bb94219b4ce8f5475dc35d3de2e432fea01a0c9610cd7f69ad05e398c6","impliedFormat":1},{"version":"ee70b8037ecdf0de6c04f35277f253663a536d7e38f1539d270e4e916d225a3f","affectsGlobalScope":true,"impliedFormat":1},{"version":"a660aa95476042d3fdcc1343cf6bb8fdf24772d31712b1db321c5a4dcc325434","impliedFormat":1},{"version":"282f98006ed7fa9bb2cd9bdbe2524595cfc4bcd58a0bb3232e4519f2138df811","impliedFormat":1},{"version":"6222e987b58abfe92597e1273ad7233626285bc2d78409d4a7b113d81a83496b","impliedFormat":1},{"version":"cbe726263ae9a7bf32352380f7e8ab66ee25b3457137e316929269c19e18a2be","impliedFormat":1},{"version":"8b96046bf5fb0a815cba6b0880d9f97b7f3a93cf187e8dcfe8e2792e97f38f87","impliedFormat":99},{"version":"bacf2c84cf448b2cd02c717ad46c3d7fd530e0c91282888c923ad64810a4d511","affectsGlobalScope":true,"impliedFormat":1},{"version":"82e687ebd99518bc63ea04b0c3810fb6e50aa6942decd0ca6f7a56d9b9a212a6","impliedFormat":99},{"version":"7f698624bbbb060ece7c0e51b7236520ebada74b747d7523c7df376453ed6fea","impliedFormat":1},{"version":"8f07f2b6514744ac96e51d7cb8518c0f4de319471237ea10cf688b8d0e9d0225","impliedFormat":1},{"version":"257b83faa134d971c738a6b9e4c47e59bb7b23274719d92197580dd662bfafc3","impliedFormat":99},{"version":"d2e64a6f25013b099e83bfadb2c388d7bef3e8f3fdb25528225bbc841e7e7e3a","impliedFormat":99},{"version":"369ba5259e66ca8c7d35e3234f7a2a0863a770fdb8266505747c65cf346a0804","impliedFormat":99},{"version":"64d984f55025daf604f670b7dfd090ea765f2098aee871174ef2ee3e94479098","impliedFormat":99},{"version":"f147b6710441cf3ec3234adf63b0593ce5e8c9b692959d21d3babc8454bcf743","impliedFormat":99},{"version":"e96d5373a66c2cfbbc7e6642cf274055aa2c7ff6bd37be7480c66faf9804db6d","impliedFormat":99},{"version":"02bcdd7a76c5c1c485cbf05626d24c86ac8f9a1d8dc31f8924108bbaa4cf3ba9","impliedFormat":99},{"version":"c874ab6feac6e0fdf9142727c9a876065777a5392f14b0bbcf869b1e69eb46b5","impliedFormat":99},{"version":"7c553fc9e34773ddbaabe0fa1367d4b109101d0868a008f11042bee24b5a925d","impliedFormat":99},{"version":"9962ce696fbdce2421d883ca4b062a54f982496625437ae4d3633376c5ad4a80","impliedFormat":99},{"version":"e3ea467c4a7f743f3548c9ed61300591965b1d12c08c8bb9aaff8a002ba95fce","impliedFormat":99},{"version":"4c17183a07a63bea2653fbfc0a942b027160ddbee823024789a415f9589de327","impliedFormat":99},{"version":"3e2203c892297ea44b87470fde51b3d48cfe3eeb6901995de429539462894464","impliedFormat":99},{"version":"c84bf7a4abc5e7fdf45971a71b25b0e0d34ccd5e720a866dd78bb71d60d41a3f","impliedFormat":99},{"version":"e01ea380015ed698c3c0e2ccd0db72f3fc3ef1abc4519f122aa1c1a8d419a505","impliedFormat":99},{"version":"5ada1f8a9580c0f7478fe03ae3e07e958f0b79bdfb9dd50eeb98c1324f40011b","impliedFormat":99},{"version":"a8301dc90b4bd9fba333226ee0f1681aeeff1bd90233a8f647e687cb4b7d3521","impliedFormat":99},{"version":"e3225dc0bec183183509d290f641786245e6652bc3dce755f7ef404060693c35","impliedFormat":99},{"version":"09a03870ed8c55d7453bc9ad684df88965f2f770f987481ca71b8a09be5205bc","impliedFormat":99},{"version":"e6233e1c976265e85aa8ad76c3881febe6264cb06ae3136f0257e1eab4a6cc5a","impliedFormat":99},{"version":"2cdd50ddc49e2d608ee848fc4ab0db9a2716624fabb4209c7c683d87e54d79c5","impliedFormat":99},{"version":"e431d664338b8470abb1750d699c7dfcebb1a25434559ef85bb96f1e82de5972","impliedFormat":99},{"version":"2c4254139d037c3caca66ce291c1308c1b5092cfcb151eb25980db932dd3b01a","impliedFormat":99},{"version":"970ae00ed018cb96352dc3f37355ef9c2d9f8aa94d7174ccd6d0ed855e462097","impliedFormat":99},{"version":"d2f8dee457ef7660b604226d471d55d927c3051766bdd80353553837492635c3","impliedFormat":99},{"version":"110a503289a2ef76141ffff3ffceb9a1c3662c32748eb9f6777a2bd0866d6fb1","impliedFormat":99},{"version":"69bf2422313487956e4dacf049f30cb91b34968912058d244cb19e4baa24da97","impliedFormat":99},{"version":"310e6b62c493ce991624169a1c1904015769d947be88dc67e00adc7ebebcfa87","impliedFormat":99},{"version":"62fefda288160bf6e435b21cc03d3fbac11193d8d3bd0e82d86623cca7691c29","impliedFormat":99},{"version":"fcc46a8bcbf9bef21023bba1995160a25f0bc590ca3563ec44c315b4f4c1b18a","impliedFormat":99},{"version":"669573548930fb7d0a0761b827e203dc623581e21febf0be80fb02414f217d74","impliedFormat":99},{"version":"f974db5be6d7428044e19c2848e72495a9b56a8d33b6fcab392e7fb5328eb8df","impliedFormat":99},{"version":"aa348c4fb2f8ac77df855f07fb66281c9f6e71746fdff3b13c7932aa7642b788","impliedFormat":99},{"version":"262d4c61b446d096f0615b5acb0a09007b2a425c6a0fd38b66e404e7b6021812","signature":"4b96dd19fd2949d28ce80e913412b0026dc421e5bf6c31d87c7b5eb11b5753b4"},{"version":"0309a01650023994ed96edbd675ea4fdc3779a823ce716ad876cc77afb792b62","impliedFormat":99},{"version":"f13d7beeea58e219daef3a40e0dc4f2bd7d9581ac04cedec236102a12dfd2090","impliedFormat":99},{"version":"48c411efce1848d1ed55de41d7deb93cbf7c04080912fd87aa517ed25ef42639","affectsGlobalScope":true,"impliedFormat":1},{"version":"a094636c05f3e75cb072684dd42cd25a4c1324bec4a866706c85c04cecd49613","affectsGlobalScope":true,"impliedFormat":99},{"version":"fe2d63fcfdde197391b6b70daf7be8c02a60afa90754a5f4a04bdc367f62793d","impliedFormat":99},{"version":"9a3e2c85ec1ab7a0874a19814cc73c691b716282cb727914093089c5a8475955","impliedFormat":99},{"version":"cbdc781d2429935c9c42acd680f2a53a9f633e8de03290ec6ea818e4f7bff19a","impliedFormat":99},{"version":"9f6d9f5dd710922f82f69abf9a324e28122b5f31ae6f6ce78427716db30a377e","impliedFormat":99},{"version":"ac2414a284bdecfd6ab7b87578744ab056cd04dd574b17853cd76830ef5b72f2","impliedFormat":99},{"version":"c3f921bbc9d2e65bd503a56fbc66da910e68467baedb0b9db0cc939e1876c0d7","impliedFormat":99},{"version":"c30a41267fc04c6518b17e55dcb2b810f267af4314b0b6d7df1c33a76ce1b330","impliedFormat":1},{"version":"72422d0bac4076912385d0c10911b82e4694fc106e2d70added091f88f0824ba","impliedFormat":1},{"version":"da251b82c25bee1d93f9fd80c5a61d945da4f708ca21285541d7aff83ecb8200","impliedFormat":1},{"version":"64db14db2bf37ac089766fdb3c7e1160fabc10e9929bc2deeede7237e4419fc8","impliedFormat":1},{"version":"98b94085c9f78eba36d3d2314affe973e8994f99864b8708122750788825c771","impliedFormat":1},{"version":"0cc99fbb161d78729d71fad66c6c363e3095862d6277160f29fa960744b785c6","affectsGlobalScope":true,"impliedFormat":99},"129a86faf021cbd7dd75371cef3025e3eb2137128b7cd35ea06b566f3413315c",{"version":"61de421bab744b4e1e105a40e0409221ba5729868cde52fbdbe3e21a1fb2c69c","impliedFormat":99},{"version":"6c84d9410cb39c418e76c74200dc943c8909863efe798df05c552933801dc42f","impliedFormat":99},"296e4d9489ff6aefc93528063dcc384338b4d6e555fbb73cdec2fee8b6744b54",{"version":"3aa67bb15606c55fdd705fc720d1c5550078cb83f21355806a72495e8f59cdfa","signature":"07415405713faaca7975ad1bfbf9b412ba297f47a6923fe9cebafd7f4dcb7395"},"6a9a61013d77df9d48372dd9f6da3b2b7395ca4675b4fb4cfbce80060beb8d86",{"version":"4f0aad57d90b3b60f110236f8aeee57237ed2c083b947e687e4412ee3040b7f6","signature":"d241dd8d3506b3a6df117db8d62114d1d2d4663baaeb0f5e72cc0e38f72725d1"},{"version":"d6cf83ebe1ae55f96a9e79a748723cdea14ad985a72f6585dc53412e32c78a86","signature":"adf2277f61af2e1981a1e63f463cb2a8fc39964908863180a4e52a88482da9e2"},{"version":"0f407ed5c25d2fb2c97a216d175a3efc0fe21fd0faf4de3cca00931016375991","signature":"46f78a03a7c6287bd7eaa736b490ad45be142f90896a3d02a739501b3d5b2de0"},{"version":"ce2c97f3e5b6a222f19f005b6ef4886d89bf4c16bc1903658802d04a552fc9d8","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"cdec8f8e76fb1dfd3a6fdca604aae2bf3f51c1110f7a0012dc5604fdbb12f689","signature":"e79eaa521c0e7a44cd476d4c4ec7a2150ffd2800d3f4a92e0c4d32eee476849b"},"af7de680832f9a99aa122282a09b27ce3ca195b913239ed42775e10797136a9d",{"version":"d3b8f898932a894fcf3a2f0137add132d5345f85c97e58cb6974a3b5f80b6127","signature":"fe68f8d72e2ba80e4e6cd7d14dc27c6463905a100d806ff4357dc083b46b60c0"},{"version":"fe03821ef6e674b9bd68b3c957a27e0eec9f2c3da4afd8174eb2a1640b2a0a3b","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"306cc4071730e408e1e6df19d616bc3b4bf5dcec55343057834231cea7e2c9b8","signature":"93ea245c6b41d0516d8e9f79df02214396c126cb7129c44a1419f591e9b0149d"},{"version":"ce71392a988633a5400e5c42a22e14144c582f2c099ac74fa6e7327728343da5","signature":"af98905d9b626d3e1e58a247fcf12eeee6dc6ab75fd66f495cfb3f53fc60442f"},{"version":"1341b7ef069321245b63656c887094a74e28ad9a4f0579d55d50b9c8ea227262","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"f046a76e6c2b6bc3a9c5cc53b99da7340b63ebd7f84e13a8a53e60f99d7c85a5","signature":"937982033f30be6d1131b84b6e07e898b512dc3c301f8fd13fbbf7fc726fc42f"},{"version":"68448904d6c46e4a04f229602ea4fe77ebe567283b1e370aefe127c03c4ae630","signature":"146b47847e0270185276ad42ff155ebe162ecf46e15dbcf5148818abd6edbb89"},{"version":"a5d5c5023804437603b51acbf146f6d7bfa9585c6addb8e45488f0e14f8ddbcc","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},"59dd6a66b9027174bc2de83cd568fc135fa912f806adc7a2fe2eee15bdb869b8","51d79ddaeeb46de8db2aa5928bf15a830b33ff0b91050c548a2ea82831010b60",{"version":"de1ffa1e3bb7fa3341e883ec6079ba6dd6f73199354948d2f7ded7dc4df74b8c","signature":"e26bccafe194370d7a6064acaa4860cf362e8b05feed9c5c0e938b2d87a7d51a"},{"version":"1dd7fa4fda5354d9a835ff15de140db2664c9ad4bba57f4fa0c8ac7b25b2f0b6","signature":"68cf7e37be5cb67a476fcf97cee522bf6991ff292ce99736568c7cf552c33f12"},{"version":"030d523e29ac43dfe92303dc59be3348b02a6d3a1769e17fde3951bdfc56b464","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"3e0fc0ac00cc2be442d8ca11f3737d52118bf68c5b54e7857cffe8c663f52b12","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},"4caa4cac2a1e939f9a5d8b39dd20a2f318f01881a3f3864a5c5af3a1529db9c1",{"version":"a41537fbd75a9b00364517e9f9a7dedb617a1b7d70529e0ddf573c2f19ef7f17","signature":"73226465e2746e4e6e679adaf07df2c26247bc2020daab13f7fbb7562276eefe"},{"version":"c5a0ffa909f5277c3830ab0ea463558b135ea049b731b3852310e0b64ca3869f","signature":"40d5d8eeb71a7cfb02078d06351f4467bdf5d8dc4d47ba467480eb30b9b73e94"},{"version":"c0efe9f1a7841088fd3b0cdb9c2290a6f5c1fa7a3aebdf3aaf270d344ff56e21","signature":"7175ca141479166d4eb613d64104eeed84093343cca30afd909912737339e111"},{"version":"ff0d9317f266d554abc684692d66c0a4cc76c2ed22248400d7838032dea5d84c","signature":"8e609bb71c20b858c77f0e9f90bb1319db8477b13f9f965f1a1e18524bf50881"},{"version":"662ff9b16378365dce8ac7752370c8c8609f11c6f5d53480c5dad6bdc00a467d","signature":"b3f301ee5acbe3a478e4b2fc5c86b1eb021f624afbf2295dbc0c605cd052de16"},"fa452635b5ac56ff89f7c2bef9ae560435d24d8f50f4ba9e661dee9e50475d28","1a017381422d08696696ee73d0b477f65817ceb7db6a862c0ee1274ec8ebf19b",{"version":"ab3408c1be7894e73630304d2c8aebea85542ca23fb96a7011032812bb2f3f90","signature":"ba54342937eb116567785cc93f6ef7c8ce3b041a67bb2ecb579ab28359b06047"},{"version":"d99de358089926db30b5837d01580cce1c517a47112e34bd386e211d6802fe74","signature":"6b55b050c9d01c4cbb1de4a608f41277e50e804ef9be262387e618cd3f75ff53"},{"version":"1ac295e91285a5df3bf0b6b06dd3324451e9e0510c5e9243763f6739fb206f31","signature":"536701639205b9046c7be694d0f58df607a5cefe8bb81b239350d70ed574e2c6"},"532ebc18ccfd0dde7741c8b3063a4d38117516b46b83b92a79587713b8e64d67","1adc127175e94440e797af519f6778fcbbd7965dbff102f9c2bb8059b3650e28",{"version":"e7f7041381a9396b1e52568564b7f90ee87f156e53a2f563f12f8a4043b13325","signature":"9e0e1dc9f88c512d6ca9044e77913ba4a6df74736ee5ea089134a65aa6375cd9"},{"version":"11c4815cba4cd9b80431a6ac938bb837610f865c8211032589b22b11cff37d53","signature":"e0b22866a9bac6f71dd2f508ee1ddd445e17ed71601d1b94ec60013fa934b48e"},{"version":"938a200cab8ef63bd63ef811d7732a0d380cc5331c865d131395e442f73fb338","signature":"db24729f7c24caafaf30635fdc17359f1e9312bd97fdbed0da9d34f00a982190"},{"version":"af5f2422c3e48b5ec5ce234c717528d39c9ff3274cc603aaa79ba1c488ffe573","signature":"a40c98740a3c1d0b71f61f6c9a7337c455d837af954e117bd0f65f0e80598fa7"},{"version":"c722f93b3272613f2645e308b41c9bd2511ceb17a69860422d09f19361542334","signature":"31e2f5b6378057657d9b6ebf2ed6de79026baef70f5463ae397768a7ef76e21d"},{"version":"17cb6cb4f714d0efd0de6e7a5254a6e6b463ced0a8c266724660ff1352da2645","signature":"599c21046dfbaec96c624b6092e7073a3472d2688490bfc8527df9a3496408ec"},{"version":"4cb8ce370dded833aa921768d43d7b5c635363c8bf8e343139df3a5554329354","signature":"c10fe759e992c8f87f199e01729cc58a5c225ecc9a914ddcb86cfc0f912c3ef1"},{"version":"9b198294f6ac383029150d5d784b7ebb2e91eff67517a13462efaca48a27cf0b","signature":"c0f8673c25e3392d2841196ba47799fa401e4135a9f37f75fedb369b79270144"},"1414615c866abf5fa5557c370fd8744a06a31e6c4403c713e17272fa2db15a91","87efe6a08272fd6ad1f8c21c2c55b64cd713a48daf94a12e14ab4030944b7ba3","304fd3f360a445e635986b3264ee8017e6a3631a2a4f6faf0d0bf7d3f98e62eb",{"version":"00c9a5c5fda50a4811bf4db39355b1043ea43e82e5e13e1a02910c6f7fb1bcea","signature":"273f5f74d3126daf5eeb15df72604de561105d1d8a2f5825c88fb09ae65eee8b"},{"version":"3531e6a9e84fa1ba01a8971a55f5b3a74817d6e7122aebfadc764775a0484660","signature":"cfc174c783fc21759487bf9e1d56afab8a6f58526c444c24ec7172160de8b71a"},{"version":"704ba60c7bb6de74ea9ee213ab33b6895f951fb1d5133bb3444f2391927e2c4c","signature":"36699080ba48ae0564aa577db1d8279ff99bd8a98720e8e26e0a88231dffbb8d"},{"version":"eaa57d44de0c8b1ce0c6fb8cf9eec693b15563cfcd71d3501c27aa0615e98667","signature":"a533121eee3903d4cc40d812648fb68cf6f62d670913ef28f5f65a179e4ca6a8"},"187c0cd60326c19be31746d530c8082dcc0b395da934fddd17ad4a356c9a417b",{"version":"d74845f8b6f02aff4b3dd36b99003dd4fe58ac2376ae511f6d92bcb3a153d6aa","signature":"6781825fe0bacb6828c87c307a6126fe3a0ae631d079cb04b8b852804fdaa7bb"},{"version":"a63d1dceca466019e671006f8f071c0bf5596206b98557239ca64f9e58c191e0","signature":"3b31470d2936e80d198c801cc596e0569736a32b31f5544a6453f3934e24fba7"},{"version":"4763b947677a33bd554af8624d932aedea06bcf57a25b11ef6a7e057210358d6","signature":"6cf67db3bb1d66e8d7dbe762fed6bdf98d8236cc313c69a42dffae94476336ab"},{"version":"a63623ab42d5e0f1b159cfcf4d80b4ab2bc324db92a6d05a6077995df1638ba0","signature":"db8a0682e6c1d96fd0116f63cde511bd7b3de4387312aabd7da4bf53d72e0ad6"},"60193090d4e861a6cc875a6f00b0616a024ac586b866dfbdf09e4ca053aa8c65","efff1f1a0df5cb30394a6323bf9fad9532cf4284de8ca3820b8c49d8bd88b463","b9aeaade08803f4f2f5e1fcf82527451afc68b6c93896a749d27d92da239344a","9da27bb4af2cd79a55068f5fa60724e96145ecd54835420f3769c1aeca2a8ebd","56900c830691def7957f3b487b19d91136aa05af0752f83673b2204303c19152","ff25d2789578679f3f1b1f5dcbb5c87a0dad6488fa583cd1fded8ae83c58b1b5",{"version":"30cdf53273913e8ee8b04cabd23d6b53df2f92265a9c88470e5d7c957d95b7ef","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},"dc575c9e3b32d510c9a974135f13f83e2b836d8bb482310c40b2495b0a763dd2",{"version":"730539759405584ef46f895820a5723593e9da5fa3a66217debc9bce8aa57ed1","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},{"version":"39422101b85c49c26174ec094efa2f65e5f4d3d26ae0d5cb1d9078b2ff157d5f","signature":"89b0f68f8f0b901f9dfff2b9e7255520283a783d6af7f2bc2953d771232317a2"},"abac25862a735319832d29b4daa0eac0af8d63e7a9f6dd0d95dcb687d8cab4a6","6e2133f8ad52b25f2ce9c7ccc30fdd2653214949e08552ee3fbd970716d07dc2","0d7c3b7a0922f5a70e863e5dc809850c9a42d9438ab0ee919657845b3db88c18","a9f7e7f88d04c20cb51988a6c5bceaf00920c6d7034f094106613a6a1f1e3160"],"root":[449,474,519,536,[539,608]],"options":{"allowJs":true,"esModuleInterop":true,"jsx":1,"module":99,"skipLibCheck":true,"strict":true,"target":9},"referencedMap":[[601,1],[602,2],[603,3],[604,4],[605,5],[606,6],[607,7],[608,8],[600,9],[570,10],[574,11],[579,12],[582,13],[590,14],[595,15],[597,16],[599,17],[572,18],[578,19],[575,20],[576,21],[577,22],[571,23],[580,24],[581,25],[573,26],[596,27],[583,28],[584,29],[587,30],[585,31],[586,29],[588,32],[589,33],[598,34],[546,35],[592,36],[591,36],[593,36],[594,37],[536,38],[541,39],[540,36],[539,36],[543,40],[542,41],[545,42],[544,36],[547,43],[549,44],[548,28],[550,45],[552,46],[551,28],[553,47],[555,48],[554,36],[557,49],[556,28],[560,50],[559,51],[561,52],[558,36],[562,53],[563,54],[564,36],[566,55],[565,36],[567,56],[569,57],[568,28],[449,58],[395,36],[475,36],[121,59],[122,59],[123,60],[76,61],[124,62],[125,63],[126,64],[71,36],[74,65],[72,36],[73,36],[127,66],[128,67],[129,68],[130,69],[131,70],[132,71],[133,71],[134,72],[135,73],[136,74],[137,75],[77,36],[75,36],[138,76],[139,77],[140,78],[174,79],[141,80],[142,36],[143,81],[144,82],[145,83],[146,84],[147,85],[148,86],[149,87],[150,88],[151,89],[152,89],[153,90],[154,36],[155,91],[156,92],[158,93],[157,94],[159,95],[160,96],[161,97],[162,98],[163,99],[164,100],[165,101],[166,102],[167,103],[168,104],[169,105],[170,106],[171,107],[78,36],[79,36],[80,36],[118,108],[119,36],[120,36],[172,109],[173,110],[178,111],[179,112],[177,35],[175,113],[176,114],[60,36],[62,115],[291,35],[522,36],[523,116],[524,117],[527,118],[526,36],[487,36],[498,119],[493,120],[496,121],[511,122],[500,36],[503,123],[502,124],[514,124],[501,125],[525,36],[495,126],[497,126],[489,127],[492,128],[508,127],[494,129],[488,36],[61,36],[482,36],[532,130],[534,131],[533,132],[531,133],[530,36],[69,134],[398,135],[403,9],[405,136],[199,137],[347,138],[374,139],[274,36],[192,36],[197,36],[338,140],[266,141],[198,36],[376,142],[377,143],[319,144],[335,145],[239,146],[342,147],[343,148],[341,149],[340,36],[339,150],[375,151],[200,152],[273,36],[275,153],[195,36],[210,154],[201,155],[214,154],[243,154],[185,154],[346,156],[356,36],[191,36],[297,157],[298,158],[292,159],[426,36],[300,36],[301,159],[293,160],[430,161],[429,162],[425,36],[379,36],[334,163],[333,36],[424,164],[294,35],[217,165],[215,166],[427,36],[428,36],[216,167],[419,168],[422,169],[226,170],[225,171],[224,172],[433,35],[223,173],[261,36],[436,36],[439,36],[438,35],[440,174],[181,36],[344,175],[345,176],[368,36],[190,177],[180,36],[183,178],[313,35],[312,179],[311,180],[302,36],[303,36],[310,36],[305,36],[308,181],[304,36],[306,182],[309,183],[307,182],[196,36],[188,36],[189,154],[397,184],[406,185],[410,186],[350,187],[349,36],[258,36],[441,188],[359,189],[295,190],[296,191],[288,192],[280,36],[286,36],[287,193],[317,194],[281,195],[318,196],[315,197],[314,36],[316,36],[270,198],[351,199],[352,200],[282,201],[283,202],[278,203],[330,204],[358,205],[361,206],[259,207],[186,208],[357,209],[182,139],[380,210],[391,211],[378,36],[390,212],[70,36],[366,213],[246,36],[276,214],[362,36],[205,36],[389,215],[194,36],[249,216],[348,217],[388,36],[382,218],[187,36],[383,219],[385,220],[386,221],[369,36],[387,208],[213,222],[367,223],[392,224],[322,36],[325,36],[323,36],[327,36],[324,36],[326,36],[328,225],[321,36],[252,226],[251,36],[257,227],[253,228],[256,229],[255,229],[254,228],[209,230],[241,231],[355,232],[442,36],[414,233],[416,234],[285,36],[415,235],[353,199],[299,199],[193,36],[242,236],[206,237],[207,238],[208,239],[204,240],[329,240],[220,240],[244,241],[221,241],[203,242],[202,36],[250,243],[248,244],[247,245],[245,246],[354,247],[290,248],[320,249],[289,250],[337,251],[336,252],[332,253],[238,254],[240,255],[237,256],[211,257],[269,36],[402,36],[268,258],[331,36],[260,259],[279,260],[277,261],[262,262],[264,263],[437,36],[263,264],[265,264],[400,36],[399,36],[401,36],[435,36],[267,265],[235,35],[68,36],[218,266],[227,36],[272,267],[212,36],[408,35],[418,268],[234,35],[412,159],[233,269],[394,270],[232,268],[184,36],[420,271],[230,35],[231,35],[222,36],[271,36],[229,272],[228,273],[219,274],[284,88],[360,88],[384,36],[364,275],[363,36],[404,36],[236,35],[396,276],[63,35],[66,277],[67,278],[64,35],[65,36],[381,279],[373,280],[372,36],[371,281],[370,36],[393,282],[407,283],[409,284],[411,285],[413,286],[417,287],[448,288],[421,288],[447,289],[423,290],[431,291],[432,292],[434,293],[443,294],[446,177],[445,36],[444,295],[538,296],[537,36],[466,297],[464,298],[465,299],[453,300],[454,298],[461,301],[452,302],[457,303],[467,36],[458,304],[463,305],[469,306],[468,307],[451,308],[459,309],[460,310],[455,311],[462,297],[456,312],[477,313],[476,314],[365,315],[450,36],[472,316],[471,36],[470,36],[473,317],[512,36],[490,36],[491,318],[58,36],[59,36],[10,36],[11,36],[13,36],[12,36],[2,36],[14,36],[15,36],[16,36],[17,36],[18,36],[19,36],[20,36],[21,36],[3,36],[22,36],[23,36],[4,36],[24,36],[28,36],[25,36],[26,36],[27,36],[29,36],[30,36],[31,36],[5,36],[32,36],[33,36],[34,36],[35,36],[6,36],[39,36],[36,36],[37,36],[38,36],[40,36],[7,36],[41,36],[46,36],[47,36],[42,36],[43,36],[44,36],[45,36],[8,36],[51,36],[48,36],[49,36],[50,36],[52,36],[9,36],[53,36],[54,36],[55,36],[57,36],[56,36],[1,36],[96,319],[106,320],[95,319],[116,321],[87,322],[86,323],[115,295],[109,324],[114,325],[89,326],[103,327],[88,328],[112,329],[84,330],[83,295],[113,331],[85,332],[90,333],[91,36],[94,333],[81,36],[117,334],[107,335],[98,336],[99,337],[101,338],[97,339],[100,340],[110,295],[92,341],[93,342],[102,343],[82,344],[105,335],[104,333],[108,36],[111,345],[509,346],[506,347],[507,346],[510,348],[505,36],[486,349],[483,350],[481,351],[479,352],[478,36],[480,353],[484,36],[485,354],[518,355],[513,356],[504,357],[499,36],[528,358],[515,359],[529,360],[516,361],[521,362],[520,363],[517,364],[535,365],[474,366],[519,367]],"affectedFilesPendingEmit":[601,602,603,604,605,606,607,608,570,574,579,582,590,595,597,599,572,578,575,576,577,571,580,581,573,596,583,584,587,585,586,588,589,598,546,592,591,593,594,536,541,540,543,542,545,544,547,549,548,550,552,551,553,555,554,557,556,560,559,561,558,562,563,564,566,565,567,569,568,474,519],"version":"5.9.3"} \ No newline at end of file diff --git a/memory/contracts.md b/memory/contracts.md index d52cbda..b98f0ce 100644 --- a/memory/contracts.md +++ b/memory/contracts.md @@ -41,6 +41,18 @@ - `POST /projects/{id}/chapters/{no}/accept` ← `AcceptRequest{final_text(min1), decisions:[ConflictDecision{conflict_index:int>=0, verdict:"accept"|"ignore"|"manual", note?}]}` → `AcceptResponse{project_id,chapter_no,accepted_version:int,digest_added:bool,decisions_recorded:int,review_id?:uuid}`。**冲突 gate**:裁决的 `conflict_index` 集合须覆盖 `range(len(最近一条 review.conflicts))`,缺判→409 `CONFLICT_UNRESOLVED` + `details.missing_conflict_indices`/`conflict_count`;无留痕或零冲突→直通。digest 提炼 tier=light(`get_digest_gateway`)在事务外(R2),单事务 promote(R4)+digest.append(#4)+set_decisions 末尾一次 commit(R3)。 - 注入缝:`build_gateway_for_tier(session, store, tier)`(原 `build_writer_gateway` 退化为 writer 特例) + `get_review_gateway`/`get_digest_gateway`/`get_review_repo`/`get_digest_append_repo`;测试经 `app.dependency_overrides` 注 mock。 - **@frontend 行动**:`cd apps/web && pnpm gen:api` 重生成客户端含此三端点(T2.6)。 +- **已落(T3.2, 2026-06-18,snake_case)— 伏笔登记/状态端点 + 验收后到期扫描**(挂 `foreshadow.router`,prefix `/projects`,tag `foreshadow`,已注册): + - `POST /projects/{id}/foreshadow` ← `ForeshadowRegisterRequest{code(min1),title(min1),planted_at?,content?,expected_close_from?,expected_close_to?,importance?}` → 201 `ForeshadowView{code,title,status,planted_at?,content?,expected_close_from?,expected_close_to?,importance?,links:list,progress:list}`(status=OPEN)。重复 code(DB 唯一 `(project_id,code)`)→ **422** `VALIDATION`+`details{field:"code",code,reason:"duplicate"}`。 + - `PATCH /projects/{id}/foreshadow/{code}` ← `ForeshadowTransitionRequest{to_status?,progress_entry?}` → 200 `ForeshadowView`。非法转移→422 `VALIDATION`+`details{reason:"invalid_transition"}`;二者都缺→422 `empty_update`;code 不存在→404 `NOT_FOUND`。先转移后追加进展,端点写后 `commit()`(repo 只 flush)。 + - **错误码取舍**:重复/非法转移映射 `VALIDATION`(422) 非 409——现有唯一 409 码 `CONFLICT_UNRESOLVED` 语义专指未决冲突禁验收,不复用;未动 `packages/shared/errors.py` 契约。若 T3.5/前端需真 409 须先加专用码。 + - **验收后到期扫描**:`POST .../accept` 单事务 commit 成功后经 `BackgroundTasks` 登记 `run_overdue_scan(session_factory, project_id, chapter_no=刚验收章号)`——**自建独立 session**(请求 session 已关闭)、应用 `current_ch>expected_close_to AND status≠CLOSED→OVERDUE`、有变更才 commit;日志 `foreshadow_overdue_scan`(带 request_id/overdue_count/codes),失败吞不冒泡。§7.4 持久性局限(进程内/重启丢任务)原型接受、不引 jobs 表。 + - **T3.5 续接**:`GET /projects/{id}/foreshadow?status=` 看板加到本 `foreshadow.router`(注入 `get_foreshadow_repo`→`list_by_status`,建议响应包 `{foreshadow:[...]}`);`POST .../outline` 落**独立** `routers/outline.py`(别混进 foreshadow.py),调 `run_outline`(C6 扩)+`build_gateway_for_tier(...,"analyst")`,逐章 upsert `outline` 表、端点末尾 commit。 + - **@frontend 行动**:T3.6 前 `pnpm gen:api` 纳入伏笔登记/状态端点(+ T3.5 看板/outline)。 +- **已落(T3.5, 2026-06-18,snake_case)— 伏笔看板 + 大纲生成端点**: + - `GET /projects/{id}/foreshadow?status=`(tag `foreshadow`):query `status?` ∈ `OPEN|PARTIAL|CLOSED|OVERDUE`(缺省=全部,非法→422 `VALIDATION`+`details{field:"status",reason:"invalid_status"}`) → 200 `ForeshadowBoardResponse{foreshadow:list[ForeshadowView]}`(按 code 升序)。 + - `POST /projects/{id}/outline`(tag `outline`,独立 `routers/outline.py`)← `OutlineGenerateRequest{volume:int=1,ge=1}`(M3 简单分卷:整批同卷)→ 200 `OutlineResponse{chapters:list[OutlineChapterView{no,volume,beats:list[str],foreshadow_windows:list[ForeshadowWindowView{code,plant_chapter?,expected_close_from?,expected_close_to?}]}]}`;项目不存在→404 `NOT_FOUND`;无凭据/网关失败→503 `LLM_UNAVAILABLE`(流前 JSON 信封)。调 `run_outline`(analyst 网关 `get_outline_gateway`)→逐章 upsert `outline` 表(写侧 `SqlOutlineWriteRepo`,只 flush)→端点末尾 `commit()`(含 1 条 usage_ledger)。 + - **beats 存储形**:DB `outline.beats` 是 JSONB dict→写侧包成 `{"beats":[...]}` 落库,API 出参 `OutlineChapterView.beats` 解包成裸 `list[str]`。 + - **@frontend 行动**:T3.6 `pnpm gen:api` 纳入看板/大纲端点。 ## C4 · 编排器接口(LangGraph 写章图) owner @llm 状态: 稳定(M1/T1.3, 2026-06-18;M2 扩四审/验收) - 来源:`ARCHITECTURE.md §5.2`(图)/ §7.3(SSE)。**M1 仅单 `write` 节点**;并行四审/collect/interrupt(accept) 属 M2。 @@ -64,6 +76,12 @@ - **accept 不在本图**:`interrupt_before=["accept"]`/accept 节点属 T2.4(确定性事务代码,从领域表重读,R3/不变量#5)。 - 消费方行动:T2.5 跑 review 子图 + `normalize_review` + 端点**流耗尽后 `await session.commit()`**(网关 ledger + collect 均只 flush,不提交则记账/留痕静默丢失,同 M1 draft 坑);`GET .../reviews` 用 `review_repo.list_for_chapter`(新→旧)。T2.4 从 `review_repo.list_for_chapter` 读 collect 落的行(`decisions=None`)裁决。 +### C4 扩展(T3.3, 2026-06-18)· 三审齐(foreshadow + pace 并入) +- `REVIEW_SPECS = (continuity_spec, foreshadow_spec, pace_spec)`;`build_review_graph(..., review_specs=REVIEW_SPECS)` 默认三审齐(文风第四审留 M4)。`run_review`/`make_review_node` 对 spec 泛型(按 `req.output_schema` 产 parsed)。 +- collect 列映射(一次 `review_repo.record(...)` 落齐):continuity→`conflicts`(list);foreshadow→`foreshadow_sug`(list,把 `planted`/`resolved` 扁平成单 list、每条加 `kind:"planted"|"resolved"`);pace→`pace`(dict,整 `{water,hook,beat_map}` 入列);style 留 M4。任一审 incomplete→该列空/None,不阻塞其余(§5.2)。 +- `normalize_review` SSE(向后兼容,`section`/`conflict` 不变)新增:`foreshadow{kind,code?,title,where?,note?}`(每条建议一条)、`pace{water:[{where,reason}],hook:bool,beat_map:[int]}`(一条)。键按字典序确定性遍历(continuityLlmResponse`):`build_outline_request(spec, *, context, user_id, project_id)->LlmRequest`(`output_schema=OutlineResult`,`stream=False`) + `async run_outline(spec, *, context, gateway, user_id, project_id)->OutlineResult`(`gateway.run().parsed`;**只读不写库**;大纲独立生成、非并行审,网关失败**直接上抛**不隔离,parsed 违约抛 `ValueError`)。 +- 消费方:T3.5 端点调 `run_outline` 拿 `OutlineResult` → 逐章 upsert `outline` 表(`beats`/`foreshadow_windows` 入 JSONB,`volume` 由端点分卷逻辑提供,schema 不含 volume);analyst 网关复用 `build_gateway_for_tier(session,store,"analyst")`/`get_review_gateway`;记账闭环同 M2(端点末尾 `commit()`)。T3.6 前端大纲编辑器消费窗口。 + +### C6 扩展(T3.3, 2026-06-18)· foreshadow-analyst + pace-checker spec +- `foreshadow_spec`(name="foreshadow", tier="analyst", reads=["foreshadow"], writes=[]只读, output=`ForeshadowReview{planted:[ForeshadowSuggestion],resolved:[ForeshadowSuggestion]}`;`ForeshadowSuggestion{code?,title,where?,note?}`)。 +- `pace_spec`(name="pace", tier="light", reads=["rules"], writes=[]只读, output=`PaceReview{water:[PaceIssue{where,reason}],hook:bool,beat_map:[int]}`)。genre 模板 DSL = pace system_prompt 内描述黄金三章/章末钩子/爽点密度,genre 级模板经 review_context 规则合并(`rules`)注入,**未新建表/repo**。 +- 三审只读,留痕经 collect→review_repo(写库 commit 归端点)。详细图/SSE 见上「C4 扩展(T3.3)」。 ## C7 · 前端 ↔ 后端类型契约 owner @backend(产出 OpenAPI) / @frontend(生成) 状态: 待定义 - 机制:FastAPI OpenAPI → `apps/web/lib/api` TS 类型(openapi-typescript/orval)。 diff --git a/memory/decisions.md b/memory/decisions.md index ca7b604..a508d68 100644 --- a/memory/decisions.md +++ b/memory/decisions.md @@ -20,6 +20,37 @@ - 理由 / 取舍:保留既有「注入 client 便于测试」风格,结构化路径同样可注 fake。`ProviderResult.text` 对结构化路径置为 `parsed.model_dump_json()`(便于日志/留痕),程序消费走 `parsed`。 - 影响:契约 C1(`run()` 填 `parsed`;见 contracts 变更日志)、`ProviderResult` 加 `parsed` 字段;T2.2 续审节点据此消费。 +## [2026-06-18] 并行审记账并发安全选 add-only(去网关 ledger flush)— @llm (T3.8) +- 背景:三审同 superstep 并行共用请求 session,`SqlAlchemyLedgerSink.record` 的 `await flush()` 是唯一让步点→第二/三审 flush 重入「Session is already flushing」→被 `run_review` 吞成 incomplete、foreshadow/pace 静默丢失(T3.7 暴露)。 +- 选择:`record` 改 **add-only**(去 `await flush()`,只 `session.add(row)`)。`session.add()` 同步不让步→并行协程不交错;持久化仍靠端点/事务末尾 `commit()`(自动 flush 待决行)。 +- 理由 / 取舍:最小改动、单 owner(只动 `ledger.py`),不牵动 apps/(@backend)。前提已校验:①并行审区间无 DB 查询触发 autoflush(review_context 取自 state、适配器不碰 session);②draft/review/accept/outline 端点末尾均已 commit。缓冲/独立 session 方案跨 owner、更重,不取。 +- 影响:C1 边界语义不变(commit 责任方未变);凡新增「并行用网关产 usage」路径,sink 必须保持 add-only、不得在并行段 await flush。新增并发回归测试 `test_ledger_concurrency.py`。 + +## [2026-06-18] 节拍图用定高 CSS 柱 + 文本 sparkline,不引图表库 — @frontend (T3.6) +- 选择:`beat_map`(int 序列) 按本序列 min..max 线性归一到 7 档 ▁..▇(全相等→居中档,最低柱给 12% 可见高度),渲染定高 CSS 柱 + `aria-label="爽点节拍图 ▁▃▅…"`。静态无动画→天然满足 prefers-reduced-motion。 +- 理由:KISS,避免图表库依赖。影响:T3.7 节拍图 `role="img"` selector。 + +## [2026-06-18] 大纲 beats 存储形 + 写侧 repo 命名 — @backend (T3.5) +- 背景:`outline.beats` DB 列是 JSONB dict,但 outliner 产 `list[str]`;读侧 `OutlineRepo`(C5/memory)已占名。 +- 选择:写侧 `SqlOutlineWriteRepo` 把 list 包成 `{"beats":[...]}` 落库(读侧同形 round-trip 一致),API 出参 `OutlineChapterView.beats` 解包成裸 `list[str]`。写侧命名加 `Write` 前缀避撞 C5 读侧(同 DigestAppendRepo/ForeshadowLedgerRepo 先例)。`volume` 由端点提供(schema 无逐章卷号,M3 默认全卷 1)。`get_outline_gateway` 复用 `build_gateway_for_tier(...,"analyst")`、独立缝便于测试 override。 +- 影响:C3 扩(outline 端点);T3.6 渲染 beats 裸 list + 窗口徽标;T3.7 断言 outline 行 beats 列形。 + +## [2026-06-18] 验收后伏笔到期扫描挂 BackgroundTask + 重复code/非法转移映射 VALIDATION(422) — @backend (T3.2) +- 背景:§5.5 步骤3 `TODO(M3)` 要验收后置 OVERDUE;登记/状态端点需映射 DB 唯一冲突与非法状态机转移到错误信封。 +- 选择:(1) accept 端点 commit 成功后经 FastAPI BackgroundTasks 登记 `run_overdue_scan`(自建独立 session,因请求 session 已关闭);扫描抽纯 async 函数 + 可注入 `repo_factory`/`session_factory` 缝,单测直接 await 注 fake、不起后台线程。(2) 重复 code(IntegrityError)/`InvalidTransition` 都映射 `ErrorCode.VALIDATION`(422) 非 409——现有 409 码 `CONFLICT_UNRESOLVED` 语义=未决冲突,复用不符;未新增 shared 错误码。 +- 影响:C3 扩(foreshadow 登记/状态端点);§7.4 持久性局限(进程内/重启丢任务)原型接受、不引 jobs 表;T3.5 看板/outline 续接本 router;T3.7 真 pg 断言 OVERDUE。 + +## [2026-06-18] 伏笔写侧 Repository 加 `Ledger` 前缀避免与读侧同名 — @backend (T3.1) +- 背景:读侧 `ForeshadowRepo`/`ForeshadowView`/`SqlForeshadowRepo`(`domain/repositories.py`+`memory/`)已被 assemble(C5 稳定)占用且 View 最小(无 importance/links/progress)。M3 写侧账本需更全 View + 写方法。 +- 选择:写侧命名 `ForeshadowLedgerRepo`/`SqlForeshadowLedgerRepo`/`ForeshadowLedgerView`,落 `domain/foreshadow_repo.py`;读侧不动。状态机纯函数落 `domain/foreshadow_state.py`(`ForeshadowStatus` StrEnum + `transition`/`is_overdue`/`apply_overdue_scan`/`InvalidTransition`)。 +- 理由:同 T2.3 `DigestAppendRepo` vs 读侧 `DigestRepo` 先例,读写分离免歧义、不破坏 C5。写方法只 flush 不 commit(commit 归 T3.2 扫描任务 / T3.5 端点)。 +- 影响:T3.2 验收后扫描 / T3.5 看板端点消费 `ForeshadowLedger*`。 + +## [2026-06-18] 大纲节点不接进 review 图、不做失败隔离 — @llm (T3.4) +- 背景:续审在 `run_review` 内把网关失败隔离为 `incomplete`(§5.2 任一审不阻塞其余)。 +- 选择:大纲是**独立生成**(不在写章/审稿流水线),`run_outline` 裸函数、网关失败**直接上抛**由 T3.5 端点处理;带 `output_schema` 时 `parsed` 非 `OutlineResult` 抛 `ValueError`(C1 违约即显错,不返回空大纲)。 +- 影响:C6 扩(outliner spec + OutlineResult + run_outline 缝);T3.5 端点调 `run_outline` 持久化 `outline` 表。 + ## [2026-06-18] 审稿页终稿来源:页内可编辑 textarea(无 GET draft 端点)— @frontend (T2.6) - 背景:审稿/验收都需「终稿」文本,但当前无 `GET .../draft` 端点拉取已存草稿。 - 选择:审稿页用可折叠 textarea 编辑终稿(`initialDraft=''`),重审传它为 `{draft}`(空→后端回退已存草稿),验收 `final_text`=它。 diff --git a/memory/gotchas.md b/memory/gotchas.md index 93d01d2..c9c83ec 100644 --- a/memory/gotchas.md +++ b/memory/gotchas.md @@ -7,6 +7,18 @@ --- +- [2026-06-18] @llm **`SqlAlchemyLedgerSink.record` 改 add-only(T3.8 修,去 `await flush()`)**:原 add+flush 在并行审里 flush 重入炸(见下条)。改为只 `session.add(row)`——`add()` 同步不让步→并行协程不交错;持久化靠端点/事务 `commit()`(自动 flush)。draft/review/accept/outline 端点末尾均已 commit,记账不丢。**凡新增「并行用网关产 usage」的路径,sink 必须保持 add-only,不得在并行段 `await flush`。** 旧 gotcha「record 只 flush 不 commit」措辞已过时——现为「add-only,commit 归调用方」(commit 仍会 flush,故记账语义不变)。 +- [2026-06-18] @qa/@llm **并行审记账撞 session(M3 真 bug,T3.7 暴露→T3.8 修)**:三审同 LangGraph superstep 并行,各自 `gateway.run()`→共用**请求 session** 的 `SqlAlchemyLedgerSink.record`(`session.add`+`await session.flush()`)。`AsyncSession` 非并发安全→第二/三审 flush 撞 `Session is already flushing`→被 `run_review` 失败隔离吞成 `incomplete`→**foreshadow/pace 静默丢失**(SSE 无事件、`chapter_reviews.foreshadow_sug`/`pace` 列空、日志 `review_node_incomplete error='Session is already flushing'`)。M2 单审未触发。根因=并行路径里有 `await flush()`(唯一 await 的 DB-IO)。修向:审稿期记账避免并发 flush(add-only 靠端点 commit / 或缓冲后 collect 串行落 / 或并行审记账用独立 session)。 +- [2026-06-18] @qa **E2E 验「验收后 BackgroundTask 扫描」时序**:httpx `ASGITransport` 下 `await client.post(...)` 会等 ASGI app 协程(含 Starlette background tasks)跑完才返回,故 client 上下文退出后断言稳定不 flaky;必 override `get_session_factory`→`e2e_sm`(真 sessionmaker,同测试 engine/loop),否则默认 `get_sessionmaker()` 另建 engine 绑别的 loop。 +- [2026-06-18] @frontend **审稿 seed 扩成三态**:`useReviewStream.seed` 入参由 `ReviewConflict[]` 改 `ReviewSeed{conflicts,foreshadow,pace}`(进页同时种伏笔建议/节奏留痕,免重审即可看)。`ReviewStreamState` 加 `foreshadow:ForeshadowSuggestion[]`(累加) / `pace:PaceReport|null`(**替换非累加**,对齐后端 collect「pace 整 dict 入列」)。 +- [2026-06-18] @frontend **伏笔 transition 乐观更新只改 status、回滚存快照**:`useForeshadow.transition` 本地先改 status,PATCH 失败 `setItems(snapshot)` 回滚 + 读 `error.details.reason`(duplicate/invalid_transition/empty_update) 映射文案。register **不乐观**(要服务端 code 唯一校验):成功才追加返回行,重复 code→422 友好提示不改 items。大纲页**无 GET 端点**:进页 `initialChapters=[]`,靠 `POST .../outline` 生成填充(无凭据→503 引导去设置)。 +- [2026-06-18] @backend **端点级测「无凭据→LLM_UNAVAILABLE」不要靠真实 `get_*_gateway` dep 解析**——`FakeSession` 不支持 `.execute`,`SqlCredentialStore` 会炸成 500。做法:override 网关 dep 注一个 `raise AppError(LLM_UNAVAILABLE)` 的 async 函数(等价无凭据行为),与 review/accept/outline 测试一致。 +- [2026-06-18] @backend **BackgroundTask 必须自建独立 session**:FastAPI BackgroundTasks 在 response 发回、请求 session 关闭后才跑——复用 `Depends(get_session)` 的 session 已关闭会炸。验收后到期扫描经可注入 `SessionFactory`(=`get_sessionmaker()`),`run_overdue_scan` 内 `async with factory()` 开新 session 自己 commit;再加 `repo_factory` 缝便于单测注 fake、纯函数 await 不起后台线程。**accept 端点新增 `get_session_factory` 依赖→所有 accept 测试 client 需 override 它**(否则 dep 解析会建真 engine)。 +- [2026-06-18] @backend **伏笔登记重复 code / 非法转移 → `VALIDATION`(422) 非 409**:现有唯一 409 码 `CONFLICT_UNRESOLVED` 专指未决冲突禁验收,不复用;`details.reason` ∈ `duplicate`/`invalid_transition`/`empty_update` 供前端区分。register 捕 SQLAlchemy `IntegrityError`→rollback→`AppError(VALIDATION)`;transition 捕 `InvalidTransition`/`LookupError`(404)。 +- [2026-06-18] @llm **三审列类型不齐**:`chapter_reviews.foreshadow_sug` 是 JSONB **list**、`pace` 是 JSONB **dict**——collect 把 `ForeshadowReview{planted,resolved}`(dict) **扁平成单 list**、每条加 `kind:"planted"|"resolved"`;`PaceReview` 整体入 dict 列。贴合既有 DB 列类型、不改 @db。新审种加入须同步:collect 列映射 + `sse._section_result_events` 分支 + `normalize_review` 键名。`sse.py` import `.collect` 的 spec.name 常量做 section 分流(无循环:collect 不 import sse)。 +- [2026-06-18] @llm **三审并行图测试按 `req.output_schema` 路由 parsed**(`SchemaRoutingRunGateway`)——单 `FakeRunGateway` 对所有审返同一 parsed 会让三审拿错 schema。验失败隔离:某 schema 不登记→网关抛 KeyError→`run_review` 隔离为 `incomplete`,无需改 gateway。 +- [2026-06-18] @backend **伏笔 `record_progress` append JSONB 必须新建 list 重赋值**(`row.progress = [*old, entry]`),不可原地 `.append()`——SQLAlchemy 默认不侦测可变 JSONB 原地突变,原地改不脏标记→flush 丢失。`scan_overdue` 仅在有变更时 flush(空扫描零写)。状态机:`transition` 同态(current==to)幂等放行、CLOSED 为终态(离开 CLOSED 全非法抛 `InvalidTransition`);`is_overdue` 严格大于(current==expected_close_to 仍在窗口内不逾期)、无 expected_close_to 永不逾期。 +- [2026-06-18] @llm **orchestrator 内每模块各自声明 `GatewayRun` Protocol**(`review_node.py` 与 `outline_node.py` 各一份,按模块最小依赖)——**不跨模块复用、不在 orchestrator `__init__` 重复导出**(`__init__` 只导出 review_node 那个,避免 re-export 名冲突);outline 的为模块内部用。 - [2026-06-18] @qa **M2 E2E 多档位假适配器**:`config.tier_defaults` writer/analyst/light 默认同 provider(deepseek)→单个假适配器(`provider="deepseek"`)即覆盖三档位;据 `req.output_schema is ContinuityReview`(续审)/否则 digest facts schema 分支返回 `parsed`;三档位用不同 `input_tokens` 区分以断言各自落 `usage_ledger`。三端点记账闭环 = review 端点流末 commit + accept 验收事务末 commit 都把网关 ledger flush 真正提交(M1 ledger bug 在 M2 无复发)。 - [2026-06-18] @qa **E2E 验证「digest 从终稿非草稿」(#4) 手法**:final_text 注入草稿没有的标记串,假 light 适配器把它放进 digest facts 的 `summary`,断言 `chapter_digests.facts["summary"]==标记` 且 `标记 not in draft_text`。accept 409 gate 经 ASGITransport 正常返回(`AppError` 不上抛),断言 `resp.json()["error"]["details"]["missing_conflict_indices"]`(`ErrorCode` StrEnum → `"CONFLICT_UNRESOLVED"`)。 - [2026-06-18] @frontend **审稿历史 `conflicts` 在 OpenAPI 被标松散 `{[k]:unknown}[]`**(后端用 dict/JSONB 列)→ 前端 `lib/review/history.ts` 安全收窄成 `ReviewConflict{type,where,refs,suggestion}`,缺字段给默认、**保序**(顺序=冲突 gate 的 `conflict_index` 身份,不可重排,否则裁决错位)。 diff --git a/packages/agents/tests/test_outliner_spec.py b/packages/agents/tests/test_outliner_spec.py new file mode 100644 index 0000000..048f9b7 --- /dev/null +++ b/packages/agents/tests/test_outliner_spec.py @@ -0,0 +1,117 @@ +"""T3.4 大纲 Agent 契约测试:OutlineResult schema + outliner_spec 声明(C6 扩)。 + +契约测试——构造符合 schema 的 mock 响应,校验伏笔窗口字段、章节结构、 +声明式 writes(验收/T3.5 才真写库,不变量 #3)。不联网、无 DB。 +""" + +from __future__ import annotations + +import pytest +from pydantic import ValidationError +from ww_agents import ( + AgentSpec, + ForeshadowWindow, + OutlineChapter, + OutlineResult, + outliner_spec, +) + + +def test_outline_result_parses_mock_response() -> None: + # Arrange:模拟网关 instructor 校验后的结构化大纲产出 + mock = { + "chapters": [ + { + "no": 1, + "beats": ["主角登场,立下目标", "埋下身世伏笔"], + "foreshadow_windows": [ + { + "code": "FS-BIRTH", + "plant_chapter": 1, + "expected_close_from": 20, + "expected_close_to": 30, + } + ], + }, + { + "no": 2, + "beats": ["遭遇对手"], + "foreshadow_windows": [], + }, + ] + } + + # Act + outline = OutlineResult.model_validate(mock) + + # Assert + assert len(outline.chapters) == 2 + assert outline.chapters[0].no == 1 + assert outline.chapters[0].beats == ["主角登场,立下目标", "埋下身世伏笔"] + window = outline.chapters[0].foreshadow_windows[0] + assert window.code == "FS-BIRTH" + assert window.plant_chapter == 1 + assert window.expected_close_from == 20 + assert window.expected_close_to == 30 + + +def test_outline_chapter_defaults_empty_windows() -> None: + chapter = OutlineChapter.model_validate({"no": 3, "beats": ["收束支线"]}) + assert chapter.foreshadow_windows == [] + + +def test_outline_result_defaults_to_empty_chapters() -> None: + assert OutlineResult().chapters == [] + + +def test_foreshadow_window_optional_bounds_default_none() -> None: + window = ForeshadowWindow.model_validate({"code": "FS-X"}) + assert window.plant_chapter is None + assert window.expected_close_from is None + assert window.expected_close_to is None + + +def test_foreshadow_window_requires_code() -> None: + with pytest.raises(ValidationError): + ForeshadowWindow.model_validate({"plant_chapter": 1}) + + +def test_outline_chapter_requires_no() -> None: + with pytest.raises(ValidationError): + OutlineChapter.model_validate({"beats": ["x"]}) + + +def test_outliner_spec_is_analyst_tier() -> None: + assert outliner_spec.tier == "analyst" + assert outliner_spec.name == "outliner" + + +def test_outliner_spec_declares_expected_reads() -> None: + assert outliner_spec.reads == [ + "projects", + "foreshadow", + "characters", + "world_entities", + ] + + +def test_outliner_spec_declares_outline_write() -> None: + # 声明式 writes(经验收/T3.5 才真写库,不变量 #3) + assert outliner_spec.writes == ["outline"] + + +def test_outliner_spec_output_schema_is_outline_result() -> None: + assert outliner_spec.output_schema is OutlineResult + + +def test_outliner_spec_has_nonempty_system_prompt() -> None: + assert outliner_spec.system_prompt.strip() + + +def test_outliner_spec_is_immutable() -> None: + with pytest.raises(ValidationError): + outliner_spec.tier = "writer" + + +def test_outliner_spec_is_agent_spec() -> None: + assert isinstance(outliner_spec, AgentSpec) diff --git a/packages/agents/tests/test_review_specs.py b/packages/agents/tests/test_review_specs.py new file mode 100644 index 0000000..022b8c7 --- /dev/null +++ b/packages/agents/tests/test_review_specs.py @@ -0,0 +1,167 @@ +"""T3.3 三审契约测试:foreshadow / pace spec + 输出 schema(C6 扩)。 + +契约测试——构造符合 schema 的 mock 响应,校验字段、tier、四审只读(writes==[])。 +不联网、无 DB。 +""" + +from __future__ import annotations + +import pytest +from pydantic import ValidationError +from ww_agents import ( + AgentSpec, + ForeshadowReview, + ForeshadowSuggestion, + PaceIssue, + PaceReview, + foreshadow_spec, + pace_spec, +) + +# ---- ForeshadowReview schema ---- + + +def test_foreshadow_review_parses_mock_response() -> None: + # Arrange:模拟网关 instructor 校验后的结构化产出 + mock = { + "planted": [ + { + "code": "FS-MARK", + "title": "胸口的胎记", + "where": "第 2 段主角沐浴时被瞥见", + "note": "暗示皇族血统", + } + ], + "resolved": [ + { + "code": "FS-BIRTH", + "title": "身世之谜", + "where": "第 9 段老者道破来历", + "note": "回收第 1 章埋设", + } + ], + } + + # Act + review = ForeshadowReview.model_validate(mock) + + # Assert + assert len(review.planted) == 1 + assert review.planted[0].code == "FS-MARK" + assert review.resolved[0].code == "FS-BIRTH" + assert review.resolved[0].where == "第 9 段老者道破来历" + + +def test_foreshadow_review_defaults_to_empty_lists() -> None: + review = ForeshadowReview() + assert review.planted == [] + assert review.resolved == [] + + +def test_foreshadow_suggestion_only_title_required() -> None: + sug = ForeshadowSuggestion.model_validate({"title": "新埋的悬念"}) + assert sug.title == "新埋的悬念" + assert sug.code is None + assert sug.where is None + assert sug.note is None + + +def test_foreshadow_suggestion_requires_title() -> None: + with pytest.raises(ValidationError): + ForeshadowSuggestion.model_validate({"code": "FS-X"}) + + +# ---- PaceReview schema ---- + + +def test_pace_review_parses_mock_response() -> None: + mock = { + "water": [{"where": "第 4–6 段反复描写天气", "reason": "信息密度低、与主线无关"}], + "hook": True, + "beat_map": [1, 3, 2, 0, 4, 5], + } + + review = PaceReview.model_validate(mock) + + assert len(review.water) == 1 + assert review.water[0].reason == "信息密度低、与主线无关" + assert review.hook is True + assert review.beat_map == [1, 3, 2, 0, 4, 5] + + +def test_pace_review_defaults() -> None: + review = PaceReview() + assert review.water == [] + assert review.hook is False + assert review.beat_map == [] + + +def test_pace_issue_requires_where_and_reason() -> None: + with pytest.raises(ValidationError): + PaceIssue.model_validate({"where": "x"}) + + +# ---- foreshadow_spec 声明 ---- + + +def test_foreshadow_spec_is_analyst_tier() -> None: + assert foreshadow_spec.tier == "analyst" + assert foreshadow_spec.name == "foreshadow" + + +def test_foreshadow_spec_is_read_only() -> None: + # 不变量 #3:四审只读 + assert foreshadow_spec.writes == [] + + +def test_foreshadow_spec_reads_foreshadow() -> None: + assert foreshadow_spec.reads == ["foreshadow"] + + +def test_foreshadow_spec_output_schema() -> None: + assert foreshadow_spec.output_schema is ForeshadowReview + + +def test_foreshadow_spec_has_nonempty_system_prompt() -> None: + assert foreshadow_spec.system_prompt.strip() + + +# ---- pace_spec 声明 ---- + + +def test_pace_spec_is_light_tier() -> None: + # 不变量 #2:节奏审用轻量档,只声明 tier + assert pace_spec.tier == "light" + assert pace_spec.name == "pace" + + +def test_pace_spec_is_read_only() -> None: + assert pace_spec.writes == [] + + +def test_pace_spec_reads_rules() -> None: + # genre 模板 DSL 经 rules(genre 级)注入 + assert pace_spec.reads == ["rules"] + + +def test_pace_spec_output_schema() -> None: + assert pace_spec.output_schema is PaceReview + + +def test_pace_spec_has_nonempty_system_prompt() -> None: + assert pace_spec.system_prompt.strip() + + +# ---- 共性:immutable + 是 AgentSpec ---- + + +def test_review_specs_are_agent_specs() -> None: + assert isinstance(foreshadow_spec, AgentSpec) + assert isinstance(pace_spec, AgentSpec) + + +def test_review_specs_are_immutable() -> None: + with pytest.raises(ValidationError): + foreshadow_spec.tier = "writer" + with pytest.raises(ValidationError): + pace_spec.tier = "writer" diff --git a/packages/agents/ww_agents/__init__.py b/packages/agents/ww_agents/__init__.py index 23125d1..c053b3a 100644 --- a/packages/agents/ww_agents/__init__.py +++ b/packages/agents/ww_agents/__init__.py @@ -1,17 +1,44 @@ """ww-agents:内置 Agent 声明(AgentSpec)+ 结构化输出 schema(C6)。 -对齐 ARCH §5.1(AgentSpec)/ §5.4(I/O 契约)/ §6.1(冲突分类)。 +对齐 ARCH §5.1(AgentSpec)/ §5.4(I/O 契约)/ §6.1(冲突分类)/ §6.2 §6.4(伏笔/节奏)。 """ from __future__ import annotations -from .schemas import Conflict, ConflictType, ContinuityReview -from .specs import AgentSpec, continuity_spec +from .schemas import ( + Conflict, + ConflictType, + ContinuityReview, + ForeshadowReview, + ForeshadowSuggestion, + ForeshadowWindow, + OutlineChapter, + OutlineResult, + PaceIssue, + PaceReview, +) +from .specs import ( + AgentSpec, + continuity_spec, + foreshadow_spec, + outliner_spec, + pace_spec, +) __all__ = [ "AgentSpec", "Conflict", "ConflictType", "ContinuityReview", + "ForeshadowReview", + "ForeshadowSuggestion", + "ForeshadowWindow", + "OutlineChapter", + "OutlineResult", + "PaceIssue", + "PaceReview", "continuity_spec", + "foreshadow_spec", + "outliner_spec", + "pace_spec", ] diff --git a/packages/agents/ww_agents/schemas.py b/packages/agents/ww_agents/schemas.py index cc783b9..848ddb7 100644 --- a/packages/agents/ww_agents/schemas.py +++ b/packages/agents/ww_agents/schemas.py @@ -44,3 +44,106 @@ class ContinuityReview(BaseModel): default_factory=list, description="检出的一致性冲突;无冲突时为空列表", ) + + +# ---- 大纲 Agent 结构化输出(ARCH §5.4 outliner 行 / §6.2 伏笔窗口)---- + + +class ForeshadowWindow(BaseModel): + """伏笔回收窗口:关联本章与某条伏笔(ARCH §6.2)。 + + 排大纲时把伏笔的「埋设章 → 期望回收区间」绑到具体章节;接近回收窗口的章节 + 提示作者推进/收束。下界/上界可缺省(仅埋设、尚未排定回收时间)。 + """ + + code: str = Field(description="伏笔编码(关联 foreshadow.code)") + plant_chapter: int | None = Field( + default=None, description="埋设章号(伏笔在本章/此章首次出现)" + ) + expected_close_from: int | None = Field(default=None, description="期望回收区间下界(章号)") + expected_close_to: int | None = Field(default=None, description="期望回收区间上界(章号)") + + +class OutlineChapter(BaseModel): + """单章大纲:章号 + 节拍清单 + 关联伏笔窗口。""" + + no: int = Field(description="章号") + beats: list[str] = Field(default_factory=list, description="本章节拍/情节要点(有序)") + foreshadow_windows: list[ForeshadowWindow] = Field( + default_factory=list, + description="本章关联的伏笔回收窗口;无则空列表", + ) + + +class OutlineResult(BaseModel): + """大纲结构化产出:分卷分章的章节大纲清单(ARCH §5.4 writes=outline)。 + + 声明式 writes(不变量 #3):本结构是排大纲的纯产物,落 `outline` 表经验收/ + 端点(T3.5),不在 agent 节点直接写库。 + """ + + chapters: list[OutlineChapter] = Field( + default_factory=list, + description="分章大纲;无则空列表", + ) + + +# ---- 伏笔续审 Agent 结构化输出(ARCH §5.4 foreshadow-analyst 行 / §6.2)---- + + +class ForeshadowSuggestion(BaseModel): + """单条伏笔建议:本章新埋线 或 疑似回收(ARCH §6.2)。 + + 只读建议——作者在验收裁决里确认后才登记/改状态(不变量 #3);本审不写库。 + `code` 可缺(新埋线尚无编码时由作者命名);`where` 给本章定位。 + """ + + code: str | None = Field( + default=None, description="伏笔编码(关联已登记 foreshadow.code;新埋可缺)" + ) + title: str = Field(description="伏笔标题/一句话描述") + where: str | None = Field(default=None, description="本章定位:哪一段埋下/疑似回收") + note: str | None = Field(default=None, description="建议说明(埋设要点 / 回收理由)") + + +class ForeshadowReview(BaseModel): + """伏笔续审结构化产出:本章新埋 + 疑似回收两组建议(ARCH §5.4 / §6.2)。 + + 只读建议清单(不变量 #3);登记/状态变更经验收裁决(作者确认)。 + """ + + planted: list[ForeshadowSuggestion] = Field( + default_factory=list, + description="本章疑似新埋的伏笔;无则空列表", + ) + resolved: list[ForeshadowSuggestion] = Field( + default_factory=list, + description="本章疑似回收/收束的伏笔;无则空列表", + ) + + +# ---- 节奏续审 Agent 结构化输出(ARCH §5.4 pace-checker 行 / §6.4)---- + + +class PaceIssue(BaseModel): + """单条节奏问题:注水段定位 + 原因(ARCH §6.4)。""" + + where: str = Field(description="本章定位:哪一段疑似注水/拖沓") + reason: str = Field(description="判定理由(信息密度低 / 重复 / 偏题等)") + + +class PaceReview(BaseModel): + """节奏续审结构化产出:注水段 + 章末钩子 + 爽点节拍图(ARCH §5.4 / §6.4)。 + + 只读(不变量 #3)。`beat_map` 是逐段节拍强度序列(整数),供前端 ▁▃▅ 可视化。 + """ + + water: list[PaceIssue] = Field( + default_factory=list, + description="疑似注水/拖沓段;无则空列表", + ) + hook: bool = Field(default=False, description="章末是否有有效钩子(悬念/反转/期待)") + beat_map: list[int] = Field( + default_factory=list, + description="逐段爽点节拍强度序列(整数,供前端节拍图可视化)", + ) diff --git a/packages/agents/ww_agents/specs.py b/packages/agents/ww_agents/specs.py index 8db5487..baed907 100644 --- a/packages/agents/ww_agents/specs.py +++ b/packages/agents/ww_agents/specs.py @@ -12,7 +12,12 @@ from __future__ import annotations from pydantic import BaseModel, ConfigDict, Field from ww_llm_gateway.types import Tier -from .schemas import ContinuityReview +from .schemas import ( + ContinuityReview, + ForeshadowReview, + OutlineResult, + PaceReview, +) class AgentSpec(BaseModel): @@ -63,3 +68,99 @@ continuity_spec = AgentSpec( writes=[], # 只读(不变量 #3) scope="builtin", ) + + +OUTLINER_SYSTEM_PROMPT = """你是长篇连载小说的「大纲架构师」。职责:依据作品立意、\ +人物与世界观,排出分卷分章的章节大纲,并为每条伏笔标注回收窗口。 + +输入材料: +- 作品设定(projects:题材、立意、主线、卖点); +- 已登记伏笔(foreshadow:编码、标题、埋设/期望回收线索); +- 主要人物(characters)与世界观实体(world_entities)。 + +产出要求(每章): +- 章号有序、按卷推进; +- beats:本章核心节拍/情节要点,推动主线、服务人物弧光; +- foreshadow_windows:本章关联的伏笔回收窗口——给出伏笔编码、埋设章号、\ +期望回收区间(下界/上界章号)。把每条伏笔的「埋设 → 回收」绑到具体章节。 + +纪律: +- 接近某伏笔的回收窗口时,在对应章 beats 里安排推进/收束该伏笔的情节,避免伏笔悬置过久; +- 你只产结构化大纲(章节 + 节拍 + 伏笔窗口),**不改稿、不写库**(落 outline 表经验收/端点); +- 伏笔编码须与已登记 foreshadow.code 对应,便于后续按窗口提示与校验。""" + + +outliner_spec = AgentSpec( + name="outliner", + tier="analyst", # 不变量 #2:只声明档位,不写 model + system_prompt=OUTLINER_SYSTEM_PROMPT, + input_schema=None, # 注入材料为序列化文本(设定/伏笔/人物/世界观),非结构化入参 + output_schema=OutlineResult, + reads=["projects", "foreshadow", "characters", "world_entities"], + writes=["outline"], # 声明式(经验收/T3.5 才真写库,不变量 #3) + scope="builtin", +) + + +FORESHADOW_SYSTEM_PROMPT = """你是长篇连载小说的「伏笔续审」(foreshadow-analyst)。\ +职责:把本章草稿与作品已登记伏笔逐项比对,找出本章**新埋的伏笔**与**疑似回收/收束**\ +的伏笔,产出结构化建议清单。 + +比对依据(注入材料): +- 已登记伏笔(foreshadow:编码、标题、状态、期望回收窗口); +- 本章草稿正文。 + +产出两组建议: +- planted(新埋):本章疑似首次埋下的伏笔——给出标题、本章定位、要点说明;\ +若与某已登记编码相关则填 code,否则留空由作者命名。 +- resolved(回收):本章疑似回收/收束已登记伏笔——给出对应 code、本章定位、回收理由。 + +纪律: +- 你**只读、只产建议**,不改稿、不写库、不直接登记或改伏笔状态——\ +登记/状态变更经作者在验收时裁决确认(不变量 #3 / #4)。 +- 只报有据可依的;无则两组都返回空列表,不要臆造。 +- 引用具体(伏笔编码、本章段落定位),便于作者就地确认。""" + + +foreshadow_spec = AgentSpec( + name="foreshadow", + tier="analyst", # 不变量 #2:只声明档位,不写 model + system_prompt=FORESHADOW_SYSTEM_PROMPT, + input_schema=None, # 注入材料为序列化文本(已登记伏笔 + 草稿),非结构化入参 + output_schema=ForeshadowReview, + reads=["foreshadow"], + writes=[], # 只读(不变量 #3) + scope="builtin", +) + + +PACE_SYSTEM_PROMPT = """你是长篇连载小说的「节奏续审」(pace-checker)。职责:按题材的\ +节奏模板审本章草稿,找出注水段、判定章末钩子有无、给出爽点节拍图。 + +题材节奏模板(genre 级规则,注入材料里带本作题材的具体模板;无则用网文通用基线): +- 黄金三章:开篇前三章须快速立爽点/钩子/代入感,信息密度高、少铺垫慢热; +- 章末钩子:每章结尾应留悬念/反转/期待,驱动追更; +- 爽点密度:按题材基线维持爽点节拍,避免长段平淡注水。 + +产出: +- water(注水段):逐段审,标出信息密度低/重复/偏题/拖沓的段落,给本章定位与原因; +- hook(章末钩子):本章结尾是否存在有效钩子(true/false); +- beat_map(节拍图):把本章按段切分,给每段一个爽点强度整数(如 0–5),\ +形成逐段强度序列,供前端 ▁▃▅ 可视化节奏起伏。 + +纪律: +- 你**只读、只报节奏诊断**,不改稿、不写库(不变量 #3)。 +- 依据题材模板判定,不臆造;无注水段则 water 为空列表。 +- beat_map 长度应与切分段数一致、顺序即正文顺序,便于前端对齐渲染。""" + + +pace_spec = AgentSpec( + name="pace", + tier="light", # 不变量 #2:只声明档位,不写 model(节奏审用轻量档) + system_prompt=PACE_SYSTEM_PROMPT, + input_schema=None, # 注入材料为序列化文本(题材模板 + 草稿),非结构化入参 + output_schema=PaceReview, + reads=["rules"], # genre 模板 DSL 经 rules(genre 级)注入,复用 review_context 规则合并 + writes=[], # 只读(不变量 #3) + scope="builtin", +) diff --git a/packages/core/tests/fakes_orchestrator.py b/packages/core/tests/fakes_orchestrator.py index ea8ee51..648c197 100644 --- a/packages/core/tests/fakes_orchestrator.py +++ b/packages/core/tests/fakes_orchestrator.py @@ -77,6 +77,29 @@ class FakeRunGateway: ) +class SchemaRoutingRunGateway: + """按 `req.output_schema` 路由返对应 `parsed`——并行三审各拿自己 schema 的产物。 + + `by_schema`:{schema 类型: 该 schema 的实例}。命中即回该 parsed;未命中抛 KeyError + (测试要保证三审 schema 都登记)。统计每个 schema 的调用次数。 + """ + + def __init__(self, by_schema: dict[type[BaseModel], BaseModel]) -> None: + self._by_schema = by_schema + self.calls: list[type[BaseModel] | None] = [] + + async def run(self, req: LlmRequest) -> LlmResponse: + schema = req.output_schema + self.calls.append(schema) + parsed = self._by_schema[schema] if schema is not None else None + return LlmResponse( + text=parsed.model_dump_json() if parsed is not None else "", + parsed=parsed, + usage=_stub_usage(), + served_by=ServedBy(provider="fake", model="fake-analyst"), + ) + + class FailingRunGateway: """`run` 抛指定异常——验「某审失败不阻塞其余」+ 未完成占位归一。""" diff --git a/packages/core/tests/test_foreshadow_repo.py b/packages/core/tests/test_foreshadow_repo.py new file mode 100644 index 0000000..0a2749e --- /dev/null +++ b/packages/core/tests/test_foreshadow_repo.py @@ -0,0 +1,293 @@ +"""T3.1 伏笔账本写侧 repo 单测(ARCH §6.2)。 + +纯内存 fake,无 DB:断言 register 唯一、list_by_status 过滤、record_progress append +不覆盖、transition 走状态机校验、scan_overdue 命中集合、按 project_id 隔离、frozen +View 不可变。`(project_id, code)` 唯一性是 **DB 级**(models UniqueConstraint),纯 fake +不引 pg 断言——真实 DB 路径由 T3.7 E2E 覆盖。 +""" + +from __future__ import annotations + +import uuid +from dataclasses import dataclass, field +from typing import Any + +import pytest +from pydantic import ValidationError +from ww_core.domain.foreshadow_repo import ForeshadowLedgerRepo, ForeshadowLedgerView +from ww_core.domain.foreshadow_state import ( + CLOSED, + OVERDUE, + PARTIAL, + InvalidTransition, +) + +PROJECT = uuid.UUID("00000000-0000-0000-0000-000000000001") +OTHER = uuid.UUID("00000000-0000-0000-0000-000000000002") + + +@dataclass +class _Row: + project_id: uuid.UUID + code: str + title: str + status: str + planted_at: int | None + content: str | None + expected_close_from: int | None + expected_close_to: int | None + importance: str | None + links: list[Any] + progress: list[Any] + + +def _view(row: _Row) -> ForeshadowLedgerView: + return ForeshadowLedgerView( + code=row.code, + title=row.title, + status=row.status, + planted_at=row.planted_at, + content=row.content, + expected_close_from=row.expected_close_from, + expected_close_to=row.expected_close_to, + importance=row.importance, + links=list(row.links), + progress=list(row.progress), + ) + + +@dataclass +class FakeForeshadowLedgerRepo: + """内存替身——镜像 SqlForeshadowLedgerRepo 语义(按 (project_id, code) 唯一)。""" + + rows: list[_Row] = field(default_factory=list) + + def _find(self, project_id: uuid.UUID, code: str) -> _Row | None: + return next((r for r in self.rows if r.project_id == project_id and r.code == code), None) + + async def register( + self, + project_id: uuid.UUID, + *, + code: str, + title: str, + planted_at: int | None = None, + content: str | None = None, + expected_close_from: int | None = None, + expected_close_to: int | None = None, + importance: str | None = None, + ) -> ForeshadowLedgerView: + if self._find(project_id, code) is not None: + raise ValueError(f"duplicate foreshadow code: {code}") + row = _Row( + project_id=project_id, + code=code, + title=title, + status="OPEN", + planted_at=planted_at, + content=content, + expected_close_from=expected_close_from, + expected_close_to=expected_close_to, + importance=importance, + links=[], + progress=[], + ) + self.rows.append(row) + return _view(row) + + async def get(self, project_id: uuid.UUID, code: str) -> ForeshadowLedgerView | None: + row = self._find(project_id, code) + return _view(row) if row is not None else None + + async def list_by_status( + self, project_id: uuid.UUID, status: str | None = None + ) -> list[ForeshadowLedgerView]: + return [ + _view(r) + for r in self.rows + if r.project_id == project_id and (status is None or r.status == status) + ] + + async def transition( + self, project_id: uuid.UUID, code: str, *, to_status: str + ) -> ForeshadowLedgerView: + from ww_core.domain.foreshadow_state import ForeshadowStatus + from ww_core.domain.foreshadow_state import transition as apply_transition + + row = self._find(project_id, code) + if row is None: + raise LookupError(f"foreshadow not found: {code}") + row.status = apply_transition(ForeshadowStatus(row.status), ForeshadowStatus(to_status)) + return _view(row) + + async def record_progress( + self, project_id: uuid.UUID, code: str, *, entry: dict[str, Any] + ) -> ForeshadowLedgerView: + row = self._find(project_id, code) + if row is None: + raise LookupError(f"foreshadow not found: {code}") + row.progress = [*row.progress, dict(entry)] + return _view(row) + + async def scan_overdue( + self, project_id: uuid.UUID, *, current_chapter: int + ) -> list[ForeshadowLedgerView]: + from ww_core.domain.foreshadow_state import ForeshadowStatus, apply_overdue_scan + + changed: list[ForeshadowLedgerView] = [] + for r in self.rows: + if r.project_id != project_id: + continue + new = apply_overdue_scan( + ForeshadowStatus(r.status), + current_chapter=current_chapter, + expected_close_to=r.expected_close_to, + ) + if new != r.status: + r.status = new + changed.append(_view(r)) + return changed + + +def _repo() -> FakeForeshadowLedgerRepo: + return FakeForeshadowLedgerRepo() + + +# ---- register ---- + + +async def test_register_starts_open() -> None: + repo: ForeshadowLedgerRepo = _repo() + view = await repo.register( + PROJECT, code="F1", title="神秘玉佩", planted_at=3, expected_close_to=20 + ) + assert isinstance(view, ForeshadowLedgerView) + assert view.code == "F1" + assert view.status == "OPEN" + assert view.planted_at == 3 + assert view.expected_close_to == 20 + assert view.progress == [] + + +async def test_register_duplicate_code_rejected() -> None: + repo: ForeshadowLedgerRepo = _repo() + await repo.register(PROJECT, code="F1", title="a") + with pytest.raises(ValueError): + await repo.register(PROJECT, code="F1", title="b") + + +async def test_register_same_code_other_project_ok() -> None: + repo: ForeshadowLedgerRepo = _repo() + await repo.register(PROJECT, code="F1", title="a") + other = await repo.register(OTHER, code="F1", title="b") + assert other.code == "F1" + + +# ---- get / list_by_status ---- + + +async def test_get_returns_none_when_absent() -> None: + repo: ForeshadowLedgerRepo = _repo() + assert await repo.get(PROJECT, "nope") is None + + +async def test_list_by_status_filters() -> None: + repo: ForeshadowLedgerRepo = _repo() + await repo.register(PROJECT, code="A", title="a") + b = await repo.register(PROJECT, code="B", title="b") + assert b.status == "OPEN" + await repo.transition(PROJECT, "B", to_status=PARTIAL) + + opens = await repo.list_by_status(PROJECT, "OPEN") + assert {v.code for v in opens} == {"A"} + partials = await repo.list_by_status(PROJECT, PARTIAL) + assert {v.code for v in partials} == {"B"} + + +async def test_list_by_status_none_returns_all() -> None: + repo: ForeshadowLedgerRepo = _repo() + await repo.register(PROJECT, code="A", title="a") + await repo.register(PROJECT, code="B", title="b") + assert len(await repo.list_by_status(PROJECT)) == 2 + + +async def test_list_by_status_isolated_by_project() -> None: + repo: ForeshadowLedgerRepo = _repo() + await repo.register(PROJECT, code="A", title="a") + await repo.register(OTHER, code="B", title="b") + rows = await repo.list_by_status(PROJECT) + assert len(rows) == 1 and rows[0].code == "A" + + +# ---- transition (走状态机) ---- + + +async def test_transition_applies_state_machine() -> None: + repo: ForeshadowLedgerRepo = _repo() + await repo.register(PROJECT, code="A", title="a") + v = await repo.transition(PROJECT, "A", to_status=PARTIAL) + assert v.status == "PARTIAL" + v2 = await repo.transition(PROJECT, "A", to_status=CLOSED) + assert v2.status == "CLOSED" + + +async def test_transition_rejects_illegal() -> None: + repo: ForeshadowLedgerRepo = _repo() + await repo.register(PROJECT, code="A", title="a") + await repo.transition(PROJECT, "A", to_status=CLOSED) + with pytest.raises(InvalidTransition): + await repo.transition(PROJECT, "A", to_status=PARTIAL) + + +# ---- record_progress (append-only) ---- + + +async def test_record_progress_appends_not_overwrites() -> None: + repo: ForeshadowLedgerRepo = _repo() + await repo.register(PROJECT, code="A", title="a") + await repo.record_progress(PROJECT, "A", entry={"chapter": 5, "note": "first hint"}) + v = await repo.record_progress(PROJECT, "A", entry={"chapter": 9, "note": "second"}) + assert len(v.progress) == 2 + assert v.progress[0]["chapter"] == 5 + assert v.progress[1]["chapter"] == 9 + + +# ---- scan_overdue ---- + + +async def test_scan_overdue_marks_due_rows() -> None: + repo: ForeshadowLedgerRepo = _repo() + await repo.register(PROJECT, code="DUE", title="d", expected_close_to=10) + await repo.register(PROJECT, code="NOTYET", title="n", expected_close_to=30) + await repo.register(PROJECT, code="NOWIN", title="w") # 无窗口 + changed = await repo.scan_overdue(PROJECT, current_chapter=15) + assert {v.code for v in changed} == {"DUE"} + assert all(v.status == "OVERDUE" for v in changed) + # NOTYET / NOWIN 保持 OPEN + assert (await repo.get(PROJECT, "NOTYET")).status == "OPEN" # type: ignore[union-attr] + + +async def test_scan_overdue_skips_closed() -> None: + repo: ForeshadowLedgerRepo = _repo() + await repo.register(PROJECT, code="C", title="c", expected_close_to=10) + await repo.transition(PROJECT, "C", to_status=CLOSED) + changed = await repo.scan_overdue(PROJECT, current_chapter=99) + assert changed == [] + assert (await repo.get(PROJECT, "C")).status == "CLOSED" # type: ignore[union-attr] + + +async def test_scan_overdue_isolated_by_project() -> None: + repo: ForeshadowLedgerRepo = _repo() + await repo.register(OTHER, code="X", title="x", expected_close_to=5) + changed = await repo.scan_overdue(PROJECT, current_chapter=99) + assert changed == [] # OTHER 的行不受 PROJECT 扫描影响 + + +# ---- frozen View ---- + + +async def test_view_is_frozen() -> None: + repo: ForeshadowLedgerRepo = _repo() + v = await repo.register(PROJECT, code="A", title="a") + with pytest.raises(ValidationError): + v.status = OVERDUE diff --git a/packages/core/tests/test_foreshadow_state.py b/packages/core/tests/test_foreshadow_state.py new file mode 100644 index 0000000..aec52e0 --- /dev/null +++ b/packages/core/tests/test_foreshadow_state.py @@ -0,0 +1,143 @@ +"""T3.1 伏笔状态机纯函数单测(ARCH §6.2)。 + +确定性纯函数,无 IO:覆盖全转移(登记/首次照应/完成回收)+ OVERDUE 到期判据边界 +(current ==/ expected_close_to;CLOSED 不被覆盖;无 expected_close_to 不 OVERDUE)+ +非法转移显式处理。 +""" + +from __future__ import annotations + +import pytest +from ww_core.domain.foreshadow_state import ( + CLOSED, + OPEN, + OVERDUE, + PARTIAL, + ForeshadowStatus, + InvalidTransition, + apply_overdue_scan, + is_overdue, + transition, +) + +# ---- 合法转移(登记→OPEN→PARTIAL→CLOSED)---- + + +def test_open_to_partial() -> None: + assert transition(OPEN, PARTIAL) == PARTIAL + + +def test_partial_to_closed() -> None: + assert transition(PARTIAL, CLOSED) == CLOSED + + +def test_open_to_closed_direct_recover() -> None: + # 一步回收(埋后直接收)也合法 + assert transition(OPEN, CLOSED) == CLOSED + + +def test_overdue_to_closed_recover_after_due() -> None: + # 逾期后仍可回收 + assert transition(OVERDUE, CLOSED) == CLOSED + + +def test_overdue_to_partial_partial_recover_after_due() -> None: + assert transition(OVERDUE, PARTIAL) == PARTIAL + + +# ---- 非法转移显式拒绝 ---- + + +def test_closed_cannot_reopen() -> None: + with pytest.raises(InvalidTransition): + transition(CLOSED, OPEN) + + +def test_closed_cannot_become_overdue_via_transition() -> None: + with pytest.raises(InvalidTransition): + transition(CLOSED, OVERDUE) + + +def test_closed_cannot_become_partial() -> None: + with pytest.raises(InvalidTransition): + transition(CLOSED, PARTIAL) + + +def test_partial_cannot_go_back_to_open() -> None: + with pytest.raises(InvalidTransition): + transition(PARTIAL, OPEN) + + +def test_same_status_is_noop_allowed() -> None: + # 幂等:同态转移允许(看板手动设回同值不报错) + assert transition(OPEN, OPEN) == OPEN + assert transition(PARTIAL, PARTIAL) == PARTIAL + + +# ---- is_overdue 判据边界 ---- + + +def test_is_overdue_strictly_past_window() -> None: + assert is_overdue(current_chapter=11, expected_close_to=10, status=OPEN) is True + + +def test_is_overdue_at_window_boundary_not_overdue() -> None: + # current == expected_close_to:仍在窗口内,未逾期 + assert is_overdue(current_chapter=10, expected_close_to=10, status=OPEN) is False + + +def test_is_overdue_before_window_not_overdue() -> None: + assert is_overdue(current_chapter=5, expected_close_to=10, status=OPEN) is False + + +def test_is_overdue_closed_never_overdue() -> None: + assert is_overdue(current_chapter=99, expected_close_to=10, status=CLOSED) is False + + +def test_is_overdue_partial_can_be_overdue() -> None: + assert is_overdue(current_chapter=11, expected_close_to=10, status=PARTIAL) is True + + +def test_is_overdue_without_window_never_overdue() -> None: + # 无 expected_close_to → 永不逾期 + assert is_overdue(current_chapter=999, expected_close_to=None, status=OPEN) is False + + +def test_is_overdue_already_overdue_stays() -> None: + assert is_overdue(current_chapter=11, expected_close_to=10, status=OVERDUE) is True + + +# ---- apply_overdue_scan ---- + + +def test_apply_overdue_scan_sets_overdue_when_due() -> None: + new = apply_overdue_scan(OPEN, current_chapter=11, expected_close_to=10) + assert new == OVERDUE + + +def test_apply_overdue_scan_keeps_status_when_not_due() -> None: + assert apply_overdue_scan(OPEN, current_chapter=10, expected_close_to=10) == OPEN + + +def test_apply_overdue_scan_does_not_touch_closed() -> None: + # CLOSED 不被 OVERDUE 覆盖(不变量) + assert apply_overdue_scan(CLOSED, current_chapter=99, expected_close_to=10) == CLOSED + + +def test_apply_overdue_scan_no_window_keeps_status() -> None: + assert apply_overdue_scan(PARTIAL, current_chapter=99, expected_close_to=None) == PARTIAL + + +def test_apply_overdue_scan_idempotent_on_overdue() -> None: + assert apply_overdue_scan(OVERDUE, current_chapter=12, expected_close_to=10) == OVERDUE + + +# ---- 枚举值与 DB 文本一致 ---- + + +def test_status_values_match_db_text() -> None: + assert OPEN == "OPEN" + assert PARTIAL == "PARTIAL" + assert CLOSED == "CLOSED" + assert OVERDUE == "OVERDUE" + assert {s.value for s in ForeshadowStatus} == {"OPEN", "PARTIAL", "CLOSED", "OVERDUE"} diff --git a/packages/core/tests/test_outline_node.py b/packages/core/tests/test_outline_node.py new file mode 100644 index 0000000..ea14ffb --- /dev/null +++ b/packages/core/tests/test_outline_node.py @@ -0,0 +1,137 @@ +"""T3.4 大纲节点单测——build_outline_request + run_outline(C6 扩 / ARCH §5.4 §6.2)。 + +注入 mock 网关(产 `OutlineResult` parsed),无真 LLM、无真 Postgres。 +大纲节点是裸函数(独立生成,不在写章/审稿流水线);只产结构化大纲,**不写库** +(持久化属 T3.5 验收/端点,不变量 #3)。asyncio_mode=auto。 +""" + +from __future__ import annotations + +import uuid + +import pytest +from fakes_orchestrator import FailingRunGateway, FakeRunGateway +from ww_agents import ( + ForeshadowWindow, + OutlineChapter, + OutlineResult, + outliner_spec, +) +from ww_core.orchestrator import build_outline_request, run_outline + +PROJECT = uuid.UUID("00000000-0000-0000-0000-000000000001") +USER = uuid.UUID("00000000-0000-0000-0000-0000000000aa") + +CONTEXT = "## 立意\n复仇成长\n## 已开伏笔\nFS-BIRTH: 身世之谜(待第20-30章回收)" + +_OUTLINE = OutlineResult( + chapters=[ + OutlineChapter( + no=1, + beats=["主角登场", "埋身世伏笔"], + foreshadow_windows=[ + ForeshadowWindow( + code="FS-BIRTH", + plant_chapter=1, + expected_close_from=20, + expected_close_to=30, + ) + ], + ), + OutlineChapter(no=2, beats=["遭遇对手"], foreshadow_windows=[]), + ] +) + + +# ---- build_outline_request:tier 不变量、缓存断点、output_schema、run 非 stream ---- + + +def test_build_outline_request_uses_spec_tier_and_cached_prompt() -> None: + req = build_outline_request(outliner_spec, context=CONTEXT, user_id=USER, project_id=PROJECT) + + assert req.tier == "analyst" # 不变量②:spec 档位透传,不传 model + assert req.stream is False # 排大纲用 run() 非 stream() + assert len(req.system) == 1 + assert req.system[0].text == outliner_spec.system_prompt + assert req.system[0].cache is True # 不变量#9 + assert req.input == CONTEXT + assert req.output_schema is OutlineResult + assert req.scope.user_id == USER + assert req.scope.project_id == PROJECT + + +# ---- run_outline:返回结构化大纲含 foreshadow_windows,只读不写库 ---- + + +async def test_run_outline_returns_structured_outline() -> None: + gateway = FakeRunGateway(_OUTLINE) + + result = await run_outline( + outliner_spec, context=CONTEXT, gateway=gateway, user_id=USER, project_id=PROJECT + ) + + assert isinstance(result, OutlineResult) + assert len(result.chapters) == 2 + assert result.chapters[0].no == 1 + window = result.chapters[0].foreshadow_windows[0] + assert window.code == "FS-BIRTH" + assert window.expected_close_to == 30 + assert gateway.call_count == 1 + + +async def test_run_outline_forwards_constructed_request() -> None: + gateway = FakeRunGateway(_OUTLINE) + + await run_outline( + outliner_spec, context=CONTEXT, gateway=gateway, user_id=USER, project_id=PROJECT + ) + + req = gateway.last_request + assert req is not None + assert req.tier == "analyst" + assert req.output_schema is OutlineResult + assert req.input == CONTEXT + + +async def test_run_outline_raises_when_gateway_fails() -> None: + # 大纲是独立生成(非流水线并行审);网关失败直接上抛,端点/T3.5 处理。 + gateway = FailingRunGateway(RuntimeError("provider down")) + + with pytest.raises(RuntimeError): + await run_outline( + outliner_spec, + context=CONTEXT, + gateway=gateway, + user_id=USER, + project_id=PROJECT, + ) + + +async def test_run_outline_raises_when_parsed_missing() -> None: + # 带 output_schema 时 parsed 必非 None(C1);若网关违约返回 None,明确报错而非静默。 + class _NoParseGateway: + async def run(self, req: object) -> object: + from ww_llm_gateway.types import LlmResponse, ServedBy, Usage + + return LlmResponse( + text="{}", + parsed=None, + usage=Usage( + provider="fake", + model="fake-analyst", + input_tokens=1, + output_tokens=1, + cost_minor=0, + currency="USD", + ), + served_by=ServedBy(provider="fake", model="fake-analyst"), + ) + + with pytest.raises(ValueError, match="parsed"): + await run_outline( + outliner_spec, + context=CONTEXT, + gateway=_NoParseGateway(), # type: ignore[arg-type] + user_id=USER, + project_id=PROJECT, + ) diff --git a/packages/core/tests/test_outline_write_repo.py b/packages/core/tests/test_outline_write_repo.py new file mode 100644 index 0000000..20b9efb --- /dev/null +++ b/packages/core/tests/test_outline_write_repo.py @@ -0,0 +1,125 @@ +"""T3.5 大纲写侧 repo 单测(ARCH §5.4 / §7.2)。 + +纯内存 fake,无 DB:断言 upsert 幂等覆盖、按 project_id 隔离、frozen View 不可变、 +`_to_view` 的 beats 包裹/解包 round-trip。`(project_id, chapter_no)` 唯一性是 **DB 级** +(models UniqueConstraint),纯 fake 不引 pg 断言——真实 DB 路径由 T3.7 E2E 覆盖。 +""" + +from __future__ import annotations + +import uuid +from dataclasses import dataclass, field +from typing import Any + +import pytest +from pydantic import ValidationError +from ww_core.domain.outline_write_repo import ( + OutlineWriteRepo, + OutlineWriteView, + _to_view, +) + +PROJECT = uuid.UUID("00000000-0000-0000-0000-000000000001") +OTHER = uuid.UUID("00000000-0000-0000-0000-000000000002") + + +@dataclass +class _Row: + """模拟 ORM Outline 行(beats 是 JSONB dict、foreshadow_windows 是 JSONB list)。""" + + project_id: uuid.UUID + volume: int + chapter_no: int + beats: dict[str, Any] | None + foreshadow_windows: list[Any] = field(default_factory=list) + + +@dataclass +class FakeOutlineWriteRepo: + """实现 `OutlineWriteRepo` Protocol(内存;幂等覆盖同 (project_id, chapter_no))。""" + + rows: dict[tuple[uuid.UUID, int], _Row] = field(default_factory=dict) + + async def upsert_chapter( + self, + project_id: uuid.UUID, + *, + volume: int, + chapter_no: int, + beats: list[str], + foreshadow_windows: list[dict[str, Any]], + ) -> OutlineWriteView: + row = self.rows.get((project_id, chapter_no)) + beats_payload = {"beats": list(beats)} + windows = [dict(w) for w in foreshadow_windows] + if row is None: + row = _Row( + project_id=project_id, + volume=volume, + chapter_no=chapter_no, + beats=beats_payload, + foreshadow_windows=windows, + ) + else: + row.volume = volume + row.beats = beats_payload + row.foreshadow_windows = windows + self.rows[(project_id, chapter_no)] = row + return _to_view(row) # type: ignore[arg-type] + + +def _repo() -> OutlineWriteRepo: + return FakeOutlineWriteRepo() + + +@pytest.mark.asyncio +async def test_upsert_inserts_new_chapter() -> None: + repo = _repo() + view = await repo.upsert_chapter( + PROJECT, + volume=1, + chapter_no=1, + beats=["a", "b"], + foreshadow_windows=[{"code": "F1", "expected_close_to": 10}], + ) + assert view.chapter_no == 1 + assert view.volume == 1 + assert view.beats == ["a", "b"] + assert view.foreshadow_windows[0]["code"] == "F1" + + +@pytest.mark.asyncio +async def test_upsert_overwrites_same_chapter() -> None: + repo = _repo() + await repo.upsert_chapter(PROJECT, volume=1, chapter_no=1, beats=["old"], foreshadow_windows=[]) + view = await repo.upsert_chapter( + PROJECT, volume=2, chapter_no=1, beats=["new"], foreshadow_windows=[] + ) + assert view.beats == ["new"] + assert view.volume == 2 + + +@pytest.mark.asyncio +async def test_upsert_isolated_by_project() -> None: + repo = FakeOutlineWriteRepo() + await repo.upsert_chapter(PROJECT, volume=1, chapter_no=1, beats=["p"], foreshadow_windows=[]) + await repo.upsert_chapter(OTHER, volume=1, chapter_no=1, beats=["o"], foreshadow_windows=[]) + assert len(repo.rows) == 2 + + +def test_to_view_unpacks_wrapped_beats() -> None: + row = _Row(project_id=PROJECT, volume=1, chapter_no=3, beats={"beats": ["x", "y"]}) + view = _to_view(row) # type: ignore[arg-type] + assert view.beats == ["x", "y"] + + +def test_to_view_handles_null_beats() -> None: + row = _Row(project_id=PROJECT, volume=1, chapter_no=3, beats=None) + view = _to_view(row) # type: ignore[arg-type] + assert view.beats == [] + + +def test_write_view_is_frozen() -> None: + view = OutlineWriteView(chapter_no=1, volume=1, beats=["a"]) + with pytest.raises(ValidationError): + view.volume = 2 diff --git a/packages/core/tests/test_review.py b/packages/core/tests/test_review.py index 12edc8c..391ffa5 100644 --- a/packages/core/tests/test_review.py +++ b/packages/core/tests/test_review.py @@ -13,16 +13,31 @@ from fakes_orchestrator import ( FailingRunGateway, FakeReviewRepo, FakeRunGateway, + SchemaRoutingRunGateway, ) from langchain_core.runnables import RunnableConfig from langgraph.checkpoint.memory import MemorySaver -from ww_agents import Conflict, ContinuityReview, continuity_spec +from ww_agents import ( + Conflict, + ContinuityReview, + ForeshadowReview, + ForeshadowSuggestion, + PaceIssue, + PaceReview, + continuity_spec, + foreshadow_spec, + pace_spec, +) from ww_core.orchestrator import ( CONTINUITY, EVENT_CONFLICT, EVENT_DONE, EVENT_ERROR, + EVENT_FORESHADOW, + EVENT_PACE, EVENT_SECTION, + FORESHADOW, + PACE, REVIEW_INCOMPLETE, REVIEW_OK, SECTION_DONE, @@ -33,6 +48,8 @@ from ww_core.orchestrator import ( build_review_request, collect_reviews, extract_conflicts, + extract_foreshadow_sug, + extract_pace, make_review_node, normalize_review, run_review, @@ -242,3 +259,177 @@ async def test_normalize_review_maps_unexpected_error_to_error_event() -> None: assert events[-1].event == EVENT_ERROR assert events[-1].data["code"] == ErrorCode.INTERNAL assert events[-1].data["request_id"] == "req-9" + + +# ============================================================================ +# T3.3 三审齐:foreshadow + pace 并入并行审 / collect 列映射 / SSE surface +# ============================================================================ + +_FORESHADOW = ForeshadowReview( + planted=[ + ForeshadowSuggestion(code="FS-MARK", title="胸口胎记", where="第2段", note="皇族暗示") + ], + resolved=[ForeshadowSuggestion(code="FS-BIRTH", title="身世", where="第9段", note="回收")], +) +_PACE = PaceReview( + water=[PaceIssue(where="第4-6段", reason="信息密度低")], + hook=True, + beat_map=[1, 3, 5, 0, 4], +) + + +def _three_reviews() -> dict[str, Any]: + """三审皆 ok 的 state['reviews'](continuity/foreshadow/pace)。""" + return { + CONTINUITY: {"status": REVIEW_OK, "result": {"conflicts": [_CONFLICT.model_dump()]}}, + FORESHADOW: {"status": REVIEW_OK, "result": _FORESHADOW.model_dump()}, + PACE: {"status": REVIEW_OK, "result": _PACE.model_dump()}, + } + + +# ---- collect 三审列映射 ---- + + +def test_extract_foreshadow_sug_flattens_planted_and_resolved() -> None: + sug = extract_foreshadow_sug(_three_reviews()) + + assert len(sug) == 2 # 1 planted + 1 resolved 扁平 + kinds = {s["kind"] for s in sug} + assert kinds == {"planted", "resolved"} + planted = next(s for s in sug if s["kind"] == "planted") + assert planted["code"] == "FS-MARK" + + +def test_extract_pace_returns_dict() -> None: + pace = extract_pace(_three_reviews()) + + assert pace is not None + assert pace["hook"] is True + assert pace["beat_map"] == [1, 3, 5, 0, 4] + + +def test_extract_foreshadow_and_pace_empty_when_incomplete() -> None: + reviews = { + FORESHADOW: {"status": REVIEW_INCOMPLETE, "result": None}, + PACE: {"status": REVIEW_INCOMPLETE, "result": None}, + } + assert extract_foreshadow_sug(reviews) == [] + assert extract_pace(reviews) is None + + +async def test_collect_records_three_audits_to_their_columns() -> None: + repo = FakeReviewRepo() + + await collect_reviews(_state(_three_reviews()), review_repo=repo) + + assert len(repo.records) == 1 # 一次 record 落齐三审 + rec = repo.records[0] + assert rec["conflicts"][0]["type"] == "能力不符" # continuity→conflicts + assert len(rec["foreshadow_sug"]) == 2 # foreshadow→foreshadow_sug(扁平) + assert rec["pace"]["hook"] is True # pace→pace + + +# ---- 三审并行图跑通(mock 网关按 schema 路由产三 parsed)---- + + +async def test_three_review_graph_runs_all_audits_end_to_end() -> None: + gateway = SchemaRoutingRunGateway( + { + ContinuityReview: ContinuityReview(conflicts=[_CONFLICT]), + ForeshadowReview: _FORESHADOW, + PaceReview: _PACE, + } + ) + repo = FakeReviewRepo() + graph = build_review_graph( + gateway, + repo, + review_specs=(continuity_spec, foreshadow_spec, pace_spec), + checkpointer=MemorySaver(), + ) + config: RunnableConfig = {"configurable": {"thread_id": "rev-3"}} + + final = await graph.ainvoke(_state(), config=config) + + assert final["reviews"][CONTINUITY]["status"] == REVIEW_OK + assert final["reviews"][FORESHADOW]["status"] == REVIEW_OK + assert final["reviews"][PACE]["status"] == REVIEW_OK + assert len(gateway.calls) == 3 # 三审各调一次网关 + # collect 一次 record 落齐三列 + rec = repo.records[0] + assert rec["conflicts"] and rec["foreshadow_sug"] and rec["pace"] + + +async def test_three_review_graph_isolates_one_failing_audit() -> None: + # foreshadow schema 缺失 → SchemaRoutingRunGateway 抛 KeyError → run_review 隔离为 incomplete + gateway = SchemaRoutingRunGateway( + { + ContinuityReview: ContinuityReview(conflicts=[_CONFLICT]), + PaceReview: _PACE, + } + ) + repo = FakeReviewRepo() + graph = build_review_graph( + gateway, + repo, + review_specs=(continuity_spec, foreshadow_spec, pace_spec), + checkpointer=MemorySaver(), + ) + config: RunnableConfig = {"configurable": {"thread_id": "rev-4"}} + + final = await graph.ainvoke(_state(), config=config) + + # foreshadow 失败被隔离,其余两审照常 + collect 照常留痕(foreshadow_sug 空) + assert final["reviews"][FORESHADOW]["status"] == REVIEW_INCOMPLETE + assert final["reviews"][CONTINUITY]["status"] == REVIEW_OK + assert final["reviews"][PACE]["status"] == REVIEW_OK + rec = repo.records[0] + assert rec["conflicts"] # continuity 落库 + assert rec["foreshadow_sug"] == [] # 失败列留空,不阻塞其余 + assert rec["pace"]["hook"] is True # pace 落库 + + +# ---- SSE:normalize_review surface 三审(section + conflict + foreshadow + pace + done)---- + + +async def test_normalize_review_surfaces_three_audits() -> None: + events = [e async for e in normalize_review(_three_reviews())] + kinds = [e.event for e in events] + + # 字典序遍历:continuity < foreshadow < pace + # continuity: section + 1 conflict + # foreshadow: section + 2(planted+resolved);pace: section + 1 + assert kinds == [ + EVENT_SECTION, + EVENT_CONFLICT, + EVENT_SECTION, + EVENT_FORESHADOW, + EVENT_FORESHADOW, + EVENT_SECTION, + EVENT_PACE, + EVENT_DONE, + ] + # foreshadow 事件带 kind + code + fs_events = [e for e in events if e.event == EVENT_FORESHADOW] + assert {e.data["kind"] for e in fs_events} == {"planted", "resolved"} + # pace 事件带节拍图 + pace_evt = next(e for e in events if e.event == EVENT_PACE) + assert pace_evt.data["beat_map"] == [1, 3, 5, 0, 4] + assert pace_evt.data["hook"] is True + assert events[-1].data == {"length": 3} # 三审项 + + +async def test_normalize_review_skips_results_for_incomplete_audit() -> None: + reviews = { + CONTINUITY: {"status": REVIEW_OK, "result": {"conflicts": []}}, + FORESHADOW: {"status": REVIEW_INCOMPLETE, "result": None}, + PACE: {"status": REVIEW_OK, "result": _PACE.model_dump()}, + } + + events = [e async for e in normalize_review(reviews)] + kinds = [e.event for e in events] + + # foreshadow incomplete → 仅 section,无 foreshadow 事件 + assert kinds == [EVENT_SECTION, EVENT_SECTION, EVENT_SECTION, EVENT_PACE, EVENT_DONE] + fs_section = next(e for e in events if e.data.get("name") == FORESHADOW) + assert fs_section.data["status"] == SECTION_INCOMPLETE diff --git a/packages/core/ww_core/domain/__init__.py b/packages/core/ww_core/domain/__init__.py index 129a641..593a95b 100644 --- a/packages/core/ww_core/domain/__init__.py +++ b/packages/core/ww_core/domain/__init__.py @@ -9,6 +9,27 @@ from ww_core.domain.chapter_repo import ( SqlChapterRepo, ) from ww_core.domain.digest_repo import DigestAppendRepo, SqlDigestAppendRepo +from ww_core.domain.foreshadow_repo import ( + ForeshadowLedgerRepo, + ForeshadowLedgerView, + SqlForeshadowLedgerRepo, +) +from ww_core.domain.foreshadow_state import ( + CLOSED, + OPEN, + OVERDUE, + PARTIAL, + ForeshadowStatus, + InvalidTransition, + apply_overdue_scan, + is_overdue, + transition, +) +from ww_core.domain.outline_write_repo import ( + OutlineWriteRepo, + OutlineWriteView, + SqlOutlineWriteRepo, +) from ww_core.domain.project_repo import ( ProjectCreate, ProjectRepo, @@ -26,7 +47,22 @@ __all__ = [ "DigestAppendRepo", "SqlDigestAppendRepo", "DigestView", + "ForeshadowLedgerRepo", + "ForeshadowLedgerView", + "SqlForeshadowLedgerRepo", + "ForeshadowStatus", + "InvalidTransition", + "OPEN", + "PARTIAL", + "CLOSED", + "OVERDUE", + "apply_overdue_scan", + "is_overdue", + "transition", "MemoryRepos", + "OutlineWriteRepo", + "OutlineWriteView", + "SqlOutlineWriteRepo", "ProjectCreate", "ProjectRepo", "ProjectView", diff --git a/packages/core/ww_core/domain/foreshadow_repo.py b/packages/core/ww_core/domain/foreshadow_repo.py new file mode 100644 index 0000000..a3f97ab --- /dev/null +++ b/packages/core/ww_core/domain/foreshadow_repo.py @@ -0,0 +1,219 @@ +"""伏笔账本**写侧** Repository(登记/状态机/进展/到期扫描;ARCH §6.2 / PRODUCT_SPEC §4.2)。 + +读侧(`list_for_codes`,供 assemble 注入伏笔窗口)已在 `ww_core.memory.sql_repositories` ++ `domain.repositories.ForeshadowRepo`/`ForeshadowView` 提供(最小 View,C5 稳定,不动)。 +本模块加**写**能力 + 看板查询,命名加 `Ledger` 前缀避免与读侧同名歧义 +(同 T2.3 `DigestAppendRepo` 区别于读侧 `DigestRepo` 的先例,见 memory/decisions)。 +`ForeshadowLedgerView` 比读侧 View 多 `importance/links/progress`(看板/账本需要)。 + +**提交边界**:所有写方法**只 `flush()` 不 `commit()`**——提交交调用方: +- T3.2 验收后扫描挂 BackgroundTask / 接 §5.5 `TODO(M3)`,由那条事务/任务提交; +- T3.5 登记/状态端点由端点事务提交。 +与 M2 验收-side repos 一致(写库副作用归编排/事务层,见 memory/gotchas)。 +含 nullable 列的唯一约束 `(project_id, code)` 不用 PG `ON CONFLICT`——`register` 仅 +显式 INSERT(重复 code 让 DB 唯一约束/调用方处理),不做 upsert(见 gotcha)。 +""" + +from __future__ import annotations + +import uuid +from typing import Any, Protocol + +from pydantic import BaseModel, Field +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from ww_db.models import Foreshadow + +from ww_core.domain.foreshadow_state import ( + ForeshadowStatus, + apply_overdue_scan, +) +from ww_core.domain.foreshadow_state import ( + transition as apply_transition, +) + + +class ForeshadowLedgerView(BaseModel): + """伏笔账本只读快照(snake_case,frozen)——看板/账本用,含进展与重要度。""" + + model_config = {"frozen": True} + + code: str + title: str + status: str + planted_at: int | None = None + content: str | None = None + expected_close_from: int | None = None + expected_close_to: int | None = None + importance: str | None = None + links: list[dict[str, Any]] = Field(default_factory=list) + progress: list[dict[str, Any]] = Field(default_factory=list) + + +class ForeshadowLedgerRepo(Protocol): + """伏笔账本写侧接口(按 project_id 隔离;写方法只 flush 不 commit)。""" + + async def register( + self, + project_id: uuid.UUID, + *, + code: str, + title: str, + planted_at: int | None = None, + content: str | None = None, + expected_close_from: int | None = None, + expected_close_to: int | None = None, + importance: str | None = None, + ) -> ForeshadowLedgerView: + """登记一条伏笔(status=OPEN);`(project_id, code)` 唯一。""" + ... + + async def get(self, project_id: uuid.UUID, code: str) -> ForeshadowLedgerView | None: + """按 code 取单条,无则 None。""" + ... + + async def list_by_status( + self, project_id: uuid.UUID, status: str | None = None + ) -> list[ForeshadowLedgerView]: + """看板查询:按状态过滤(None=全部)。供 T3.5 `GET /foreshadow?status=`。""" + ... + + async def transition( + self, project_id: uuid.UUID, code: str, *, to_status: str + ) -> ForeshadowLedgerView: + """应用状态机校验后写 `status`(非法转移抛 `InvalidTransition`)。""" + ... + + async def record_progress( + self, project_id: uuid.UUID, code: str, *, entry: dict[str, Any] + ) -> ForeshadowLedgerView: + """append 一条进展到 `progress` JSONB(append-only,不覆盖历史)。""" + ... + + async def scan_overdue( + self, project_id: uuid.UUID, *, current_chapter: int + ) -> list[ForeshadowLedgerView]: + """到期扫描:命中行置 OVERDUE,返回**被改的行**。供 T3.2 验收后扫描。""" + ... + + +def _to_view(row: Foreshadow) -> ForeshadowLedgerView: + return ForeshadowLedgerView( + code=row.code, + title=row.title, + status=row.status, + planted_at=row.planted_at, + content=row.content, + expected_close_from=row.expected_close_from, + expected_close_to=row.expected_close_to, + importance=row.importance, + links=list(row.links or []), + progress=list(row.progress or []), + ) + + +class SqlForeshadowLedgerRepo: + """SQLAlchemy 实现:登记 + 状态机转移 + 进展 append + 到期扫描(只 flush 不 commit)。""" + + def __init__(self, session: AsyncSession) -> None: + self._s = session + + async def _find(self, project_id: uuid.UUID, code: str) -> Foreshadow | None: + return ( + await self._s.execute( + select(Foreshadow).where( + Foreshadow.project_id == project_id, + Foreshadow.code == code, + ) + ) + ).scalar_one_or_none() + + async def register( + self, + project_id: uuid.UUID, + *, + code: str, + title: str, + planted_at: int | None = None, + content: str | None = None, + expected_close_from: int | None = None, + expected_close_to: int | None = None, + importance: str | None = None, + ) -> ForeshadowLedgerView: + # 仅 INSERT;唯一约束 (project_id, code) 由 DB 强制(不做 ON CONFLICT upsert,见模块注释)。 + row = Foreshadow( + project_id=project_id, + code=code, + title=title, + status=ForeshadowStatus.OPEN.value, + planted_at=planted_at, + content=content, + expected_close_from=expected_close_from, + expected_close_to=expected_close_to, + importance=importance, + links=[], + progress=[], + ) + self._s.add(row) + await self._s.flush() + await self._s.refresh(row) + return _to_view(row) + + async def get(self, project_id: uuid.UUID, code: str) -> ForeshadowLedgerView | None: + row = await self._find(project_id, code) + return _to_view(row) if row is not None else None + + async def list_by_status( + self, project_id: uuid.UUID, status: str | None = None + ) -> list[ForeshadowLedgerView]: + stmt = select(Foreshadow).where(Foreshadow.project_id == project_id) + if status is not None: + stmt = stmt.where(Foreshadow.status == status) + rows = (await self._s.execute(stmt.order_by(Foreshadow.code))).scalars() + return [_to_view(r) for r in rows] + + async def transition( + self, project_id: uuid.UUID, code: str, *, to_status: str + ) -> ForeshadowLedgerView: + row = await self._find(project_id, code) + if row is None: + raise LookupError(f"foreshadow not found: {code}") + # 状态机校验(非法转移抛 InvalidTransition,事务回滚由调用方控制)。 + row.status = apply_transition( + ForeshadowStatus(row.status), ForeshadowStatus(to_status) + ).value + await self._s.flush() + await self._s.refresh(row) + return _to_view(row) + + async def record_progress( + self, project_id: uuid.UUID, code: str, *, entry: dict[str, Any] + ) -> ForeshadowLedgerView: + row = await self._find(project_id, code) + if row is None: + raise LookupError(f"foreshadow not found: {code}") + # 不可变 append:建新 list 重赋值,确保 ORM 侦测 JSONB 变更(避免原地 mutate 不脏标记)。 + row.progress = [*(row.progress or []), dict(entry)] + await self._s.flush() + await self._s.refresh(row) + return _to_view(row) + + async def scan_overdue( + self, project_id: uuid.UUID, *, current_chapter: int + ) -> list[ForeshadowLedgerView]: + rows = ( + await self._s.execute(select(Foreshadow).where(Foreshadow.project_id == project_id)) + ).scalars() + changed: list[ForeshadowLedgerView] = [] + for row in rows: + new = apply_overdue_scan( + ForeshadowStatus(row.status), + current_chapter=current_chapter, + expected_close_to=row.expected_close_to, + ).value + if new != row.status: + row.status = new + changed.append(_to_view(row)) + if changed: + await self._s.flush() + return changed diff --git a/packages/core/ww_core/domain/foreshadow_state.py b/packages/core/ww_core/domain/foreshadow_state.py new file mode 100644 index 0000000..42b40b0 --- /dev/null +++ b/packages/core/ww_core/domain/foreshadow_state.py @@ -0,0 +1,105 @@ +"""伏笔账本状态机——确定性纯函数(ARCH §6.2 / PRODUCT_SPEC §4.2)。 + +状态机(§6.2):: + + 登记 首次照应 完成回收 + ─────▶ OPEN ──────▶ PARTIAL ─────────▶ CLOSED + │ │ + └─────────────┴──▶ OVERDUE (章号 > expected_close_to 且未 CLOSED) + (到期扫描置位; 回收后可转 CLOSED) + +- 登记 → OPEN(DB server_default)。 +- 首次照应 → PARTIAL。 +- 完成回收 → CLOSED(可从 OPEN/PARTIAL/OVERDUE 一步到达)。 +- 到期判定(M3-d 纯函数,非 Agent): + `current_chapter > expected_close_to AND status≠CLOSED → OVERDUE`。 +- **CLOSED 终态**:不被 OVERDUE 覆盖、不可重开(非法转移显式抛 `InvalidTransition`)。 + +全部为纯函数:不读 DB、不可变、可单测。写库由 `foreshadow_repo` 落地(只 flush)。 +枚举值与 DB `foreshadow.status` 文本严格一致(`StrEnum` → 直接当字符串用)。 +""" + +from __future__ import annotations + +from enum import StrEnum + + +class ForeshadowStatus(StrEnum): + """伏笔状态——值与 DB `status` 文本一致。""" + + OPEN = "OPEN" + PARTIAL = "PARTIAL" + CLOSED = "CLOSED" + OVERDUE = "OVERDUE" + + +# 便捷常量(供 repo / 测试 / 调用方按名引用,避免散落字面量)。 +OPEN = ForeshadowStatus.OPEN +PARTIAL = ForeshadowStatus.PARTIAL +CLOSED = ForeshadowStatus.CLOSED +OVERDUE = ForeshadowStatus.OVERDUE + + +class InvalidTransition(ValueError): + """非法状态转移(如 CLOSED 重开)。""" + + +# 合法转移图:from → 允许到达的状态集合(不含同态幂等,单独放行)。 +_ALLOWED: dict[ForeshadowStatus, frozenset[ForeshadowStatus]] = { + OPEN: frozenset({PARTIAL, CLOSED, OVERDUE}), + PARTIAL: frozenset({CLOSED, OVERDUE}), + OVERDUE: frozenset({PARTIAL, CLOSED}), # 逾期后仍可(部分)回收 + CLOSED: frozenset(), # 终态:不可再转移 +} + + +def transition(current: ForeshadowStatus, to_status: ForeshadowStatus) -> ForeshadowStatus: + """应用状态机校验后返回新状态(纯函数)。 + + - 同态(current == to_status)幂等放行。 + - CLOSED 为终态:任何离开 CLOSED 的转移都非法。 + - 其余按 `_ALLOWED` 校验;非法 → `InvalidTransition`。 + """ + if current == to_status: + return to_status + if to_status not in _ALLOWED[current]: + raise InvalidTransition(f"非法伏笔转移: {current.value} → {to_status.value}") + return to_status + + +def is_overdue( + *, + current_chapter: int, + expected_close_to: int | None, + status: ForeshadowStatus, +) -> bool: + """到期判据(M3-d,§6.2):`current_chapter > expected_close_to AND status≠CLOSED`。 + + - 无 `expected_close_to`(未设回收窗口)→ 永不逾期。 + - CLOSED → 永不逾期(已回收)。 + - `current_chapter == expected_close_to` → 仍在窗口内,未逾期(严格大于才算)。 + """ + if expected_close_to is None: + return False + if status == CLOSED: + return False + return current_chapter > expected_close_to + + +def apply_overdue_scan( + status: ForeshadowStatus, + *, + current_chapter: int, + expected_close_to: int | None, +) -> ForeshadowStatus: + """到期扫描应用(验收后触发,§6.2):命中判据 → 置 `OVERDUE`,否则保持原状态。 + + 幂等:已 OVERDUE 且仍逾期 → 保持 OVERDUE;CLOSED 永不被改写(is_overdue 已排除)。 + """ + if is_overdue( + current_chapter=current_chapter, + expected_close_to=expected_close_to, + status=status, + ): + return OVERDUE + return status diff --git a/packages/core/ww_core/domain/outline_write_repo.py b/packages/core/ww_core/domain/outline_write_repo.py new file mode 100644 index 0000000..8db5f57 --- /dev/null +++ b/packages/core/ww_core/domain/outline_write_repo.py @@ -0,0 +1,109 @@ +"""大纲**写侧** Repository(逐章 upsert;ARCH §5.4 outliner writes=outline / §7.2 POST /outline)。 + +读侧(`get`,供 assemble 注入本章 beats)已在 `ww_core.memory.sql_repositories.SqlOutlineRepo` ++ `domain.repositories.OutlineRepo`/`OutlineView` 提供(C5 稳定,不动)。本模块加**写**能力, +命名加 `Write` 前缀避免与读侧 `OutlineRepo` 同名歧义(同 `DigestAppendRepo`/`ForeshadowLedgerRepo` +区别于读侧的先例,见 memory/decisions)。 + +**提交边界**:`upsert_chapter` 只 `flush()` 不 `commit()`——提交交端点事务(T3.5 端点末尾 +一次 `commit()`,与网关 ledger 一并落库;与 M2 验收-side repos 一致,见 memory/gotchas)。 + +唯一约束 `(project_id, chapter_no)`:用**显式 read-modify-write**(按 chapter_no 查→有则改、 +无则插),不用 PG `ON CONFLICT`(保持与项目其它 upsert 路径一致、可纯 fake 单测,不引 pg 依赖)。 + +**beats 存储形**:DB 列 `outline.beats` 是 JSONB `dict`,而 outliner 产 `list[str]`—— +故包裹成 `{"beats": [...]}` 落库,读侧 `OutlineView.beats` 取到同形 dict(round-trip 一致)。 +`foreshadow_windows` 是 JSONB list,直接落 `[w.model_dump() for w in chapter.foreshadow_windows]`。 +""" + +from __future__ import annotations + +import uuid +from typing import Any, Protocol + +from pydantic import BaseModel, Field +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from ww_db.models import Outline + + +class OutlineWriteView(BaseModel): + """单章大纲写入后的只读快照(snake_case,frozen)。""" + + model_config = {"frozen": True} + + chapter_no: int + volume: int + beats: list[str] = Field(default_factory=list) + foreshadow_windows: list[dict[str, Any]] = Field(default_factory=list) + + +class OutlineWriteRepo(Protocol): + """大纲写侧接口(按 project_id 隔离;唯一 `(project_id, chapter_no)`;只 flush 不 commit)。""" + + async def upsert_chapter( + self, + project_id: uuid.UUID, + *, + volume: int, + chapter_no: int, + beats: list[str], + foreshadow_windows: list[dict[str, Any]], + ) -> OutlineWriteView: + """逐章 upsert:同 `(project_id, chapter_no)` 覆盖,否则插入。只 flush。""" + ... + + +def _to_view(row: Outline) -> OutlineWriteView: + raw = row.beats or {} + beats = list(raw.get("beats", [])) if isinstance(raw, dict) else [] + return OutlineWriteView( + chapter_no=row.chapter_no, + volume=row.volume, + beats=beats, + foreshadow_windows=list(row.foreshadow_windows or []), + ) + + +class SqlOutlineWriteRepo: + """SQLAlchemy 实现:显式 read-modify-write upsert(只 flush 不 commit)。""" + + def __init__(self, session: AsyncSession) -> None: + self._s = session + + async def upsert_chapter( + self, + project_id: uuid.UUID, + *, + volume: int, + chapter_no: int, + beats: list[str], + foreshadow_windows: list[dict[str, Any]], + ) -> OutlineWriteView: + row = ( + await self._s.execute( + select(Outline).where( + Outline.project_id == project_id, + Outline.chapter_no == chapter_no, + ) + ) + ).scalar_one_or_none() + # JSONB 写入建新对象重赋值(避免原地 mutate 不被 ORM 侦测,见 gotcha)。 + beats_payload = {"beats": list(beats)} + windows_payload = [dict(w) for w in foreshadow_windows] + if row is None: + row = Outline( + project_id=project_id, + volume=volume, + chapter_no=chapter_no, + beats=beats_payload, + foreshadow_windows=windows_payload, + ) + self._s.add(row) + else: + row.volume = volume + row.beats = beats_payload + row.foreshadow_windows = windows_payload + await self._s.flush() + await self._s.refresh(row) + return _to_view(row) diff --git a/packages/core/ww_core/orchestrator/__init__.py b/packages/core/ww_core/orchestrator/__init__.py index 16c4914..dad3a25 100644 --- a/packages/core/ww_core/orchestrator/__init__.py +++ b/packages/core/ww_core/orchestrator/__init__.py @@ -10,9 +10,13 @@ from __future__ import annotations from .collect import ( CONTINUITY, + FORESHADOW, + PACE, ReviewRecorder, collect_reviews, extract_conflicts, + extract_foreshadow_sug, + extract_pace, ) from .graph import ( COLLECT_NODE, @@ -22,6 +26,7 @@ from .graph import ( build_write_graph, setup_checkpointer, ) +from .outline_node import build_outline_request, run_outline from .review_node import ( REVIEW_INCOMPLETE, REVIEW_OK, @@ -36,6 +41,8 @@ from .sse import ( EVENT_CONFLICT, EVENT_DONE, EVENT_ERROR, + EVENT_FORESHADOW, + EVENT_PACE, EVENT_SECTION, EVENT_TOKEN, SECTION_DONE, @@ -45,8 +52,10 @@ from .sse import ( conflict_event, done_event, error_event, + foreshadow_event, normalize_deltas, normalize_review, + pace_event, section_event, token_event, ) @@ -59,8 +68,12 @@ __all__ = [ "EVENT_CONFLICT", "EVENT_DONE", "EVENT_ERROR", + "EVENT_FORESHADOW", + "EVENT_PACE", "EVENT_SECTION", "EVENT_TOKEN", + "FORESHADOW", + "PACE", "REVIEW_INCOMPLETE", "REVIEW_OK", "REVIEW_SPECS", @@ -74,6 +87,7 @@ __all__ = [ "GatewayStream", "ReviewRecorder", "SseEvent", + "build_outline_request", "build_review_context", "build_review_graph", "build_review_request", @@ -84,10 +98,15 @@ __all__ = [ "done_event", "error_event", "extract_conflicts", + "extract_foreshadow_sug", + "extract_pace", + "foreshadow_event", "make_review_node", "merge_reviews", "normalize_deltas", "normalize_review", + "pace_event", + "run_outline", "run_review", "section_event", "setup_checkpointer", diff --git a/packages/core/ww_core/orchestrator/collect.py b/packages/core/ww_core/orchestrator/collect.py index 1d334c5..8141cad 100644 --- a/packages/core/ww_core/orchestrator/collect.py +++ b/packages/core/ww_core/orchestrator/collect.py @@ -1,17 +1,21 @@ """collect 节点(C4 扩 / ARCH §5.2 collect 行)——汇总并行审 → 落 `chapter_reviews` 留痕。 -并行四审各把 `{spec.name: {status, result}}` 合并进 `state["reviews"]`(reducer,见 state.py); -collect 把它们汇总,调 `review_repo.record(...)` append 一行 `chapter_reviews`(真相源留痕)。 +并行审各把 `{spec.name: {status, result}}` 合并进 `state["reviews"]`(reducer,见 state.py); +collect 把它们汇总,**一次** `review_repo.record(...)` append 一行 `chapter_reviews`(真相源留痕), +按 spec.name 把各审结果映射到对应列。 不变量 #3:审稿节点只读不写库;**唯一写入点是这里经 `review_repo`**(仍是「留痕」非「生效」, -AI 产出真正入库经 T2.4 验收事务的裁决)。 +AI 产出真正入库经验收事务的裁决)。 记账边界(gotcha「网关 ledger 只 flush」):审稿经网关产 usage(网关内 `flush` 进同一 session); -collect 与 review_repo 同样**只 flush 不 commit**——`commit` 归 HTTP 端点(T2.5)/验收事务(T2.4), -与 draft 端点一致(端点在流耗尽后 `await session.commit()`)。本节点绝不 commit。 +collect 与 review_repo 同样**只 flush 不 commit**——`commit` 归 HTTP 端点 / 验收事务。 +本节点绝不 commit。 -M2 只接 continuity:从 `reviews["continuity"].result.conflicts` 抽冲突落 `conflicts` 列; -foreshadow/style/pace 列留空(M3/M4 接入后填)。设计成按 spec 名取分项,便于扩展。 +M3 三审齐 → 列映射(一次 record 落齐本章三审): +- continuity → `conflicts`(冲突清单 list); +- foreshadow → `foreshadow_sug`(planted/resolved 扁平为建议 list,每条带 `kind` 标记); +- pace → `pace`(节奏诊断 dict:water/hook/beat_map)。 +style 第四审留 M4。任一审 `incomplete`(网关失败隔离,§5.2)→ 该列留空/None,不阻塞其余。 """ from __future__ import annotations @@ -27,6 +31,12 @@ from .state import ChapterState log = structlog.get_logger(__name__) CONTINUITY = "continuity" +FORESHADOW = "foreshadow" +PACE = "pace" + +# foreshadow 建议扁平化时的来源标记(planted=新埋 / resolved=回收)。 +FORESHADOW_KIND_PLANTED = "planted" +FORESHADOW_KIND_RESOLVED = "resolved" class ReviewRecorder(Protocol): @@ -49,17 +59,53 @@ class ReviewRecorder(Protocol): ) -> Any: ... +def _ok_result(reviews: dict[str, Any], name: str) -> dict[str, Any] | None: + """取某审的结果 dict,仅当 `ok` 且有结果;否则 None(incomplete/缺席)。""" + entry = reviews.get(name) + if not entry or entry.get("status") != REVIEW_OK: + return None + result = entry.get("result") + return result if isinstance(result, dict) else None + + def extract_conflicts(reviews: dict[str, Any]) -> list[dict[str, Any]]: """从 continuity 分项抽冲突清单(纯函数)。 仅当该审 `ok` 且有结果时取其 `conflicts`;未完成/缺席 → 空列表(不臆造)。 """ - entry = reviews.get(CONTINUITY) - if not entry or entry.get("status") != REVIEW_OK: + result = _ok_result(reviews, CONTINUITY) + if result is None: return [] - result = entry.get("result") or {} - conflicts = result.get("conflicts") or [] - return [dict(c) for c in conflicts] + return [dict(c) for c in result.get("conflicts") or []] + + +def extract_foreshadow_sug(reviews: dict[str, Any]) -> list[dict[str, Any]]: + """从 foreshadow 分项扁平化建议清单(纯函数)。 + + `ForeshadowReview{planted,resolved}` → 单个 list,每条加 `kind` 标记来源 + (`foreshadow_sug` 列是 JSONB list)。未完成/缺席 → 空列表。 + """ + result = _ok_result(reviews, FORESHADOW) + if result is None: + return [] + out: list[dict[str, Any]] = [] + for sug in result.get("planted") or []: + out.append({**dict(sug), "kind": FORESHADOW_KIND_PLANTED}) + for sug in result.get("resolved") or []: + out.append({**dict(sug), "kind": FORESHADOW_KIND_RESOLVED}) + return out + + +def extract_pace(reviews: dict[str, Any]) -> dict[str, Any] | None: + """从 pace 分项取节奏诊断 dict(纯函数)。 + + `PaceReview{water,hook,beat_map}` 整体入 `pace` 列(JSONB dict)。 + 未完成/缺席 → None(列留空,§5.2 不阻塞其余)。 + """ + result = _ok_result(reviews, PACE) + if result is None: + return None + return dict(result) async def collect_reviews( @@ -67,25 +113,32 @@ async def collect_reviews( *, review_repo: ReviewRecorder, ) -> dict[str, Any]: - """collect 节点:汇总并行审 → 抽冲突 → `review_repo.record` 留痕(只 flush 不 commit)。 + """collect 节点:汇总并行审 → 各列映射 → 一次 `review_repo.record` 留痕(只 flush 不 commit)。 直接调用本函数(注入内存 fake repo)即可单测,无需图运行时。 - `chapter_version=None`:留痕行不绑具体 version(T2.4 验收时再绑/裁决,R3/R4)。 + `chapter_version=None`:留痕行不绑具体 version(验收时再绑/裁决,R3/R4)。 + 某审 incomplete → 该列空/None,不阻塞其余审落库(§5.2)。 返回空增量字典——审稿真相已在表,state 不作真相源(不变量 #5)。 """ reviews = state.get("reviews") or {} conflicts = extract_conflicts(reviews) + foreshadow_sug = extract_foreshadow_sug(reviews) + pace = extract_pace(reviews) await review_repo.record( state["project_id"], state["chapter_no"], chapter_version=None, conflicts=conflicts, + foreshadow_sug=foreshadow_sug, + pace=pace, ) log.info( "collect_reviews_recorded", project_id=str(state["project_id"]), chapter_no=state["chapter_no"], conflict_count=len(conflicts), + foreshadow_sug_count=len(foreshadow_sug), + has_pace=pace is not None, reviews=sorted(reviews.keys()), ) # 留痕已在表(真相源);不在节点 commit(端点/事务层负责,见模块 docstring)。 diff --git a/packages/core/ww_core/orchestrator/graph.py b/packages/core/ww_core/orchestrator/graph.py index c5f3753..4df18fd 100644 --- a/packages/core/ww_core/orchestrator/graph.py +++ b/packages/core/ww_core/orchestrator/graph.py @@ -15,7 +15,12 @@ from collections.abc import Sequence from typing import TYPE_CHECKING, Any from langgraph.graph import END, START, StateGraph -from ww_agents import AgentSpec, continuity_spec +from ww_agents import ( + AgentSpec, + continuity_spec, + foreshadow_spec, + pace_spec, +) from .collect import ReviewRecorder, collect_reviews from .review_node import GatewayRun, run_review @@ -51,7 +56,8 @@ def build_write_graph( return builder.compile(checkpointer=checkpointer) -REVIEW_SPECS: tuple[AgentSpec, ...] = (continuity_spec,) +# M3:三审齐(continuity + foreshadow + pace)。文风第四审属 M4/T4.2。 +REVIEW_SPECS: tuple[AgentSpec, ...] = (continuity_spec, foreshadow_spec, pace_spec) def build_review_graph( @@ -63,9 +69,10 @@ def build_review_graph( ) -> CompiledStateGraph[ChapterState, None, ChapterState, ChapterState]: """构建并编译审稿子图:`START →`(各 review spec 并行节点)`→ collect → END`。 - M2 默认仅 continuity;`review_specs` 可扩(M3/M4 加 foreshadow/style/pace 三审), - 图按这组 spec 循环加并行分支,全部汇入 collect(任一审失败不阻塞,§5.2——失败隔离 - 在 `run_review` 内、标 incomplete 占位)。 + M3 默认三审齐(continuity + foreshadow + pace);`review_specs` 仍可扩(M4 加 style + 第四审),图按这组 spec 循环加并行分支,全部汇入 collect(任一审失败不阻塞,§5.2—— + 失败隔离在 `run_review` 内、标 incomplete 占位)。`run_review`/`make_review_node` 对 spec + 泛型,三审天然支持(按 `spec.output_schema` 产 parsed,无 spec-specific 假设)。 `gateway` / `review_repo` 经闭包绑进节点(langgraph 不收 functools.partial,见 gotcha)。 **accept 不在本图**:本任务只交付到 collect(审稿留痕落 `chapter_reviews`)+ review SSE; diff --git a/packages/core/ww_core/orchestrator/outline_node.py b/packages/core/ww_core/orchestrator/outline_node.py new file mode 100644 index 0000000..fecfb31 --- /dev/null +++ b/packages/core/ww_core/orchestrator/outline_node.py @@ -0,0 +1,81 @@ +"""大纲节点(C6 扩 / ARCH §5.4 outliner 行 / §6.2 伏笔窗口)。 + +大纲是**独立生成**——不在写章/审稿流水线(不接进 review 图)。端点(T3.5)直接调 +`run_outline` 拿结构化 `OutlineResult`,再持久化到 `outline` 表(含 foreshadow_windows)。 + +确定性边界(CLAUDE.md「LangGraph」纪律): +- 节点逻辑里**无 LLM 非确定性**——不确定性藏在网关后;节点只做 + `AgentSpec + context → LlmRequest` 的纯构造 + 转发 `Gateway.run` 的 `parsed`。 +- 因此注入 mock 网关(产 `parsed`)即可单测,无需图运行时、无需真 Postgres。 + +不变量 #2:agent 只声明 `tier`,绝不传具体 model(spec.tier 透传)。 +不变量 #3:节点**只读、不写库**——结构化大纲返回调用方,落库经端点/验收(T3.5)。 +不变量 #9:`spec.system_prompt` 进缓存断点前块(cache=True);注入材料进 `input`(断点后)。 +""" + +from __future__ import annotations + +import uuid +from typing import Protocol + +import structlog +from ww_agents import AgentSpec, OutlineResult +from ww_llm_gateway.types import Block, LlmRequest, LlmResponse, Scope + +log = structlog.get_logger(__name__) + + +class GatewayRun(Protocol): + """大纲节点对网关的最小依赖——只需 `run`(注入真网关或 mock)。""" + + async def run(self, req: LlmRequest) -> LlmResponse: ... + + +def build_outline_request( + spec: AgentSpec, + *, + context: str, + user_id: uuid.UUID, + project_id: uuid.UUID, +) -> LlmRequest: + """据 spec + 注入材料构造大纲请求(纯函数)。 + + `spec.system_prompt` → `system` 缓存断点前块(cache=True,不变量 #9); + `context`(设定/伏笔/人物/世界观的序列化文本)→ `input`(断点后)。 + 排大纲用 `run()` 非 `stream()`(要结构化产物,非流式正文)。 + """ + return LlmRequest( + tier=spec.tier, # 不变量 #2:只传档位,不传 model + system=[Block(text=spec.system_prompt, cache=True)], + input=context, + output_schema=spec.output_schema, + scope=Scope(user_id=user_id, project_id=project_id), + ) + + +async def run_outline( + spec: AgentSpec, + *, + context: str, + gateway: GatewayRun, + user_id: uuid.UUID, + project_id: uuid.UUID, +) -> OutlineResult: + """跑大纲生成:构请求 → `gateway.run` → 返回结构化 `OutlineResult`。 + + 裸函数(显式 gateway 关键字),注入 mock 网关即可单测,无需图运行时。 + 大纲是独立生成(非并行审):网关失败直接上抛(端点/T3.5 处理),不静默吞。 + **只产结构化大纲、不写库**(持久化属 T3.5,不变量 #3)。 + """ + req = build_outline_request(spec, context=context, user_id=user_id, project_id=project_id) + resp = await gateway.run(req) + parsed = resp.parsed + if not isinstance(parsed, OutlineResult): + # 带 output_schema 时 parsed 必非 None(C1);违约则明确报错而非返回空大纲。 + log.error( + "outline_node_missing_parsed", + project_id=str(project_id), + parsed_type=type(parsed).__name__, + ) + raise ValueError("gateway returned no parsed OutlineResult for outline request") + return parsed diff --git a/packages/core/ww_core/orchestrator/sse.py b/packages/core/ww_core/orchestrator/sse.py index 374ccac..4bd619a 100644 --- a/packages/core/ww_core/orchestrator/sse.py +++ b/packages/core/ww_core/orchestrator/sse.py @@ -18,6 +18,7 @@ from pydantic import BaseModel from ww_llm_gateway.types import Delta from ww_shared import AppError, ErrorCode +from .collect import CONTINUITY, FORESHADOW, PACE from .review_node import REVIEW_OK log = structlog.get_logger(__name__) @@ -25,7 +26,9 @@ log = structlog.get_logger(__name__) # §7.3 事件名(全集)。完整清单以 ARCHITECTURE §7.3 为准。 EVENT_TOKEN = "token" # 正文增量(写章 draft) EVENT_SECTION = "section" # 四审分项开始/完成(审稿 review) -EVENT_CONFLICT = "conflict" # 冲突命中(审稿 review) +EVENT_CONFLICT = "conflict" # continuity 冲突命中(审稿 review) +EVENT_FORESHADOW = "foreshadow" # foreshadow 建议命中(新埋/回收,审稿 review) +EVENT_PACE = "pace" # pace 节奏诊断(注水/钩子/节拍图,审稿 review) EVENT_DONE = "done" EVENT_ERROR = "error" @@ -72,6 +75,35 @@ def conflict_event( ) +def foreshadow_event( + *, + kind: str, + code: str | None, + title: str, + where: str | None, + note: str | None, +) -> SseEvent: + """伏笔建议事件(审稿 review)——前端伏笔看板联动。 + + `kind` ∈ planted(新埋)/ resolved(回收);作者验收裁决后才登记/改状态(只读建议)。 + """ + return SseEvent( + event=EVENT_FORESHADOW, + data={"kind": kind, "code": code, "title": title, "where": where, "note": note}, + ) + + +def pace_event(*, water: list[dict[str, object]], hook: bool, beat_map: list[int]) -> SseEvent: + """节奏诊断事件(审稿 review)——前端节奏报告/节拍图 ▁▃▅ 可视化。 + + 一次审产一条:注水段清单 + 章末钩子有无 + 逐段节拍强度序列。 + """ + return SseEvent( + event=EVENT_PACE, + data={"water": water, "hook": hook, "beat_map": beat_map}, + ) + + def error_event(*, code: str, message: str, request_id: str | None = None) -> SseEvent: """错误事件,形对齐错误信封 §7.1(`code`/`message`),附 `request_id` 便于贯通排查。""" return SseEvent( @@ -122,40 +154,79 @@ async def normalize_deltas( yield done_event(length=total) +def _section_result_events(name: str, result: dict[str, Any]) -> list[SseEvent]: + """把某审 `result` dict 按审种 surface 为结构化事件(纯函数,便于单测)。 + + 畸形 result(非 dict 等)由调用方 try/except 兜成 `error` 事件;这里只按已知形读。 + """ + events: list[SseEvent] = [] + if name == CONTINUITY: + for conflict in result.get("conflicts") or []: + events.append( + conflict_event( + type=conflict.get("type", ""), + where=conflict.get("where", ""), + refs=list(conflict.get("refs") or []), + suggestion=conflict.get("suggestion", ""), + ) + ) + elif name == FORESHADOW: + for kind in ("planted", "resolved"): + for sug in result.get(kind) or []: + events.append( + foreshadow_event( + kind=kind, + code=sug.get("code"), + title=sug.get("title", ""), + where=sug.get("where"), + note=sug.get("note"), + ) + ) + elif name == PACE: + events.append( + pace_event( + water=[dict(w) for w in result.get("water") or []], + hook=bool(result.get("hook", False)), + beat_map=[int(b) for b in result.get("beat_map") or []], + ) + ) + return events + + async def normalize_review( reviews: Mapping[str, Any], *, request_id: str | None = None, ) -> AsyncIterator[SseEvent]: - """把审稿子图的结构化产出(`state["reviews"]`)归一为 SSE 事件序列。 + """把审稿子图的结构化产出(`state["reviews"]`)归一为 SSE 事件序列(M3 三审齐)。 - 供 **T2.5** 的 `POST .../review` 端点消费:跑 `build_review_graph` 后拿 `final["reviews"]` - (形 `{name: {status, result}}`),本函数产 `section`(每审 done/incomplete)+ `conflict` - (命中冲突,形对齐 C6 `Conflict`)+ `done`。HTTP event-stream 编码归 T2.5,不在此处。 + 供 `POST .../review` 端点消费:跑 `build_review_graph` 后拿 `final["reviews"]` + (形 `{name: {status, result}}`),本函数每审先发 `section`(done/incomplete),再按审种 + surface 结构化结果: + - continuity → 每冲突一条 `conflict`(形对齐 C6 `Conflict`); + - foreshadow → 每条建议一条 `foreshadow`(planted/resolved,前端看板联动); + - pace → 一条 `pace`(注水/钩子/节拍图,前端节奏报告/▁▃▅)。 + 末尾 `done{length=审项数}`。HTTP event-stream 编码归端点,不在此处。 错误处理纪律(同 `normalize_deltas`):任何意外 → 一条 `error` 事件后正常收尾,不上抛。 - 某审 `incomplete`(网关失败被隔离,§5.2)→ 发 `section{status:incomplete}`,不发其冲突。 - `reviews` 键按字典序遍历 → 确定性事件顺序(便于单测/前端稳定渲染)。 + 某审 `incomplete`(网关失败被隔离,§5.2)→ 仅发 `section{status:incomplete}`,不发其结果。 + `reviews` 键按字典序遍历 → 确定性事件顺序(便于单测/前端稳定渲染)。向后兼容: + 既有 `section`/`conflict` 形不变,仅新增 `foreshadow`/`pace`。 """ try: section_count = 0 for name in sorted(reviews.keys()): entry = reviews.get(name) or {} status = entry.get("status") - if status == REVIEW_OK: - yield section_event(name=name, status=SECTION_DONE) - section_count += 1 - result = entry.get("result") or {} - for conflict in result.get("conflicts") or []: - yield conflict_event( - type=conflict.get("type", ""), - where=conflict.get("where", ""), - refs=list(conflict.get("refs") or []), - suggestion=conflict.get("suggestion", ""), - ) - else: + if status != REVIEW_OK: yield section_event(name=name, status=SECTION_INCOMPLETE) section_count += 1 + continue + yield section_event(name=name, status=SECTION_DONE) + section_count += 1 + result = entry.get("result") or {} + for event in _section_result_events(name, result): + yield event except Exception as exc: # noqa: BLE001 — 边界兜底:任何意外都归一为 error 事件,不泄异常 log.error("sse_review_unexpected_error", error=str(exc), request_id=request_id) yield error_event( diff --git a/packages/llm_gateway/tests/test_ledger_concurrency.py b/packages/llm_gateway/tests/test_ledger_concurrency.py new file mode 100644 index 0000000..d49bb4f --- /dev/null +++ b/packages/llm_gateway/tests/test_ledger_concurrency.py @@ -0,0 +1,112 @@ +"""T3.8 回归:并行审记账并发安全(add-only,无 flush 重入)。 + +旧 bug(T3.7 真 pg E2E 暴露):三审作为同一 LangGraph superstep 的并行分支,各自 +`gateway.run()` → 共用**同一请求 session** 的 `SqlAlchemyLedgerSink.record` +(`session.add` + `await session.flush()`)。`AsyncSession` 非并发安全:`await flush()` +是并行路径里唯一让出控制权的 DB-IO,第二/三审的 flush 撞上「Session is already +flushing」(flush 重入)→ 被 `run_review` 失败隔离吞成 `incomplete` → foreshadow/pace +结果静默丢失。 + +本测试用一个会**侦测 flush 重入**的假 AsyncSession:`flush` 异步(`await asyncio.sleep` +让出控制权),若两协程同时在 flush 中即抛 `RuntimeError("Session is already flushing")` +——精确复刻 SQLAlchemy 的报错。在旧 add-then-flush 实现下,`asyncio.gather` 三并发 +`record` 会触发该重入;修复后 `record` 只 `add`(同步、不让出),不再调 flush,故三行 +全部入 session 且零重入。 +""" + +from __future__ import annotations + +import asyncio +import uuid + +import pytest +from ww_llm_gateway.ledger import SqlAlchemyLedgerSink +from ww_llm_gateway.types import Scope, Usage + + +class _FlushReentryDetectingSession: + """假 AsyncSession:记录 `add` 的行;`flush` 异步且侦测并发重入。 + + `add` 同步追加(镜像真 session)。`flush` 进入时若已有协程在 flush 中 → 抛 + `RuntimeError("Session is already flushing")`(复刻 SQLAlchemy 行为),否则 + `await asyncio.sleep(0)` 让出控制权模拟真实 DB-IO 让步点。 + """ + + def __init__(self) -> None: + self.added: list[object] = [] + self.flush_calls = 0 + self._flushing = False + + def add(self, row: object) -> None: + self.added.append(row) + + async def flush(self) -> None: + self.flush_calls += 1 + if self._flushing: + raise RuntimeError("Session is already flushing") + self._flushing = True + try: + await asyncio.sleep(0) # 让出控制权——并行协程在此交错 + finally: + self._flushing = False + + +def _usage(input_tokens: int) -> Usage: + return Usage( + provider="deepseek", + model="deepseek-chat", + input_tokens=input_tokens, + output_tokens=1, + cost_minor=0, + currency="USD", + ) + + +def _scope() -> Scope: + return Scope(user_id=uuid.UUID(int=1), project_id=uuid.UUID(int=2)) + + +async def test_concurrent_record_adds_all_rows_without_flush_reentry() -> None: + """三并发 `record`(模拟三审并行)→ 三行全部入 session,绝不抛 flush 重入。 + + 旧 add-then-flush 实现在此会因 `await flush()` 让出控制权而 flush 重入抛错; + add-only 实现下 `record` 不让出控制权,三行确定性入 session。 + """ + session = _FlushReentryDetectingSession() + sink = SqlAlchemyLedgerSink(session) # type: ignore[arg-type] + + await asyncio.gather( + sink.record(_scope(), _usage(100)), + sink.record(_scope(), _usage(200)), + sink.record(_scope(), _usage(300)), + ) + + # 三审用量全部入 session(一次调用一行,记账不丢) + assert len(session.added) == 3 + input_tokens = sorted(row.input_tokens for row in session.added) # type: ignore[attr-defined] + assert input_tokens == [100, 200, 300] + # add-only:sink 不在并行路径 flush(持久化靠端点/事务 commit) + assert session.flush_calls == 0 + + +async def test_record_does_not_flush_so_caller_commit_persists() -> None: + """单次 `record` 只 add 不 flush——契约「网关 ledger 只 add、调用方负责 commit」。""" + session = _FlushReentryDetectingSession() + sink = SqlAlchemyLedgerSink(session) # type: ignore[arg-type] + + await sink.record(_scope(), _usage(42)) + + assert len(session.added) == 1 + assert session.flush_calls == 0 + + +@pytest.mark.parametrize("n_concurrent", [2, 5, 10]) +async def test_concurrent_record_scales_without_loss(n_concurrent: int) -> None: + """N 并发 `record` 全部落 session,无丢失、无 flush 重入(不论并发度)。""" + session = _FlushReentryDetectingSession() + sink = SqlAlchemyLedgerSink(session) # type: ignore[arg-type] + + await asyncio.gather(*(sink.record(_scope(), _usage(i + 1)) for i in range(n_concurrent))) + + assert len(session.added) == n_concurrent + assert session.flush_calls == 0 diff --git a/packages/llm_gateway/ww_llm_gateway/ledger.py b/packages/llm_gateway/ww_llm_gateway/ledger.py index 1e03530..8cae3de 100644 --- a/packages/llm_gateway/ww_llm_gateway/ledger.py +++ b/packages/llm_gateway/ww_llm_gateway/ledger.py @@ -24,6 +24,19 @@ class SqlAlchemyLedgerSink: self._session = session async def record(self, scope: Scope, usage: Usage) -> None: + """把一次调用追加进 session(**只 add,不 flush/commit**)。 + + 并发安全(T3.8 bugfix):三审作为同一 LangGraph superstep 的并行分支 + 共用一个请求 session 的本 sink。`AsyncSession` 非并发安全——若在此 + `await self._session.flush()`,第二/三审的 flush 会撞上「Session is already + flushing」(flush 重入),被 `run_review` 失败隔离吞成 `incomplete`, + foreshadow/pace 结果静默丢失。`session.add()` 是同步、无 await、不让出 + 控制权,故并行协程不会在 add 中途交错。 + + 持久化靠调用方:每个端点/验收事务在流耗尽/事务末尾 `commit()`(commit + 自动 flush 全部待决行),与「网关 ledger 只 add/flush、端点或事务负责 + commit」的既有边界一致(见 gotcha「网关 ledger 只 flush、调用方必须 commit」)。 + """ row = UsageLedger( owner_id=scope.user_id, project_id=scope.project_id, @@ -36,4 +49,3 @@ class SqlAlchemyLedgerSink: currency=usage.currency, ) self._session.add(row) - await self._session.flush() diff --git a/tests/test_m3_e2e.py b/tests/test_m3_e2e.py new file mode 100644 index 0000000..4c9bb3f --- /dev/null +++ b/tests/test_m3_e2e.py @@ -0,0 +1,584 @@ +"""M3 端到端:伏笔账本/到期提醒 + 大纲(含回收窗口) + 三审(真实 DB,零 token)。 + +证明 M3 闭环(不变量 #2/#3/#4/#5): + 用例 1 · 伏笔 埋设→进展→逾期提醒(验收后扫描) + `POST /projects` → `POST .../foreshadow`(埋 F1, expected_close_to=2) + → `PATCH .../foreshadow/F1`(追加一条进展) → `PUT .../chapters/5/draft` + → `POST .../chapters/5/accept`(零冲突直通) → **验收后 BackgroundTask 到期扫描**置 + `current_ch(5) > expected_close_to(2) AND status≠CLOSED → OVERDUE` → + `GET .../foreshadow?status=OVERDUE` 含 F1 + DB 真源断言(F1.status==OVERDUE、progress 含那条)。 + 对照:未越界伏笔 F2(expected_close_to=9) 不应 OVERDUE。 + 用例 2 · 排大纲含伏笔回收窗口 + `POST .../outline` → analyst 假适配器产 `OutlineResult` → DB `outline` 表逐章行存在 + (`beats` 形 `{"beats":[...]}`、`foreshadow_windows` JSONB list 含窗口);**再次 POST 幂等 + 覆盖**(唯一 `(project_id,chapter_no)` 行数不变);`usage_ledger` 含 analyst 那次记账。 + 用例 3 · 三审 SSE(continuity/foreshadow/pace 三审齐 + 留痕列) + `POST .../chapters/N/review` → 解析帧含三审 section + ≥1 `foreshadow`/`pace` 事件; + `GET .../reviews` 留痕行 `foreshadow_sug`(list)/`pace`(dict) 列已填。 + +确定性 & 零成本(同 M1/M2):真实 `Gateway` + 多档位假适配器(据 `req.output_schema` +分支返回固定 `parsed`,绝不联网)+ 真实 `SqlAlchemyLedgerSink`(用量记账闭环走通)。 +无 pg → skip。 + +坑(见 memory/gotchas): +- `LifespanManager` 触发 lifespan → `seed_stub_user`(owner_id FK 依赖它)。 +- `get_sessionmaker` engine 绑定首个 loop → 每 DB 测试清缓存重建、结束 dispose。 +- 网关 ledger / 验收-side repos / outline upsert 均只 flush;端点/验收事务 commit 才落库。 +- **BackgroundTask 自建独立 session**:FastAPI BackgroundTasks 在 response 发回、请求 + session 关闭后才跑——`get_session_factory` 必被 override 成 `e2e_sm`(真 sessionmaker), + 否则 dep 解析会另建真 engine(跨 loop 风险)。httpx `ASGITransport` 下,`await client.post` + 调用会等 ASGI app 协程(含 background tasks)跑完才返回——故 client 上下文退出后断言稳定。 +- 多档位假适配器据 `req.output_schema` 路由 parsed(OutlineResult / 三审 schema / + ChapterDigestFacts);写章走 stream。 +""" + +from __future__ import annotations + +import json +import uuid +from collections.abc import AsyncIterator, Callable +from typing import Annotated + +import httpx +import pytest +from asgi_lifespan import LifespanManager +from fastapi import Depends +from sqlalchemy import delete, select +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker +from ww_agents import ( + ContinuityReview, + ForeshadowReview, + ForeshadowSuggestion, + ForeshadowWindow, + OutlineChapter, + OutlineResult, + PaceIssue, + PaceReview, +) +from ww_api.services.digest_extraction import ChapterDigestFacts +from ww_db import get_session, get_sessionmaker +from ww_db.models import ( + Chapter, + ChapterDigest, + ChapterReview, + Foreshadow, + Outline, + Project, + UsageLedger, +) +from ww_llm_gateway import ( + Gateway, + SqlAlchemyLedgerSink, + resolve_route, +) +from ww_llm_gateway.adapters.base import ( + Capabilities, + ProviderResult, + ProviderUsage, + StreamChunk, +) +from ww_llm_gateway.types import LlmRequest + +# 三档位(writer/analyst/light)默认都路由到 deepseek(config.tier_defaults)。 +_PROVIDER = "deepseek" + +# 确定性流式写章 token。 +_TOKENS = ["第", "五", "章", ":", "回收之时。"] + +# 终稿里可被 digest 追溯的标记串(验证 digest 从终稿提炼,不变量 #4)。 +_FINAL_MARKER = "终稿独有事件:旧伏笔在第五章浮现" + +# outliner 假产物:两章,章 1 关联伏笔窗口(回收窗口 code=F1)。 +_OUTLINE_RESULT = OutlineResult( + chapters=[ + OutlineChapter( + no=1, + beats=["开端:少年入门", "埋线:神秘玉佩"], + foreshadow_windows=[ + ForeshadowWindow( + code="F1", + plant_chapter=1, + expected_close_from=2, + expected_close_to=2, + ) + ], + ), + OutlineChapter( + no=2, + beats=["试炼:初次交锋"], + foreshadow_windows=[], + ), + ] +) + +# 三审固定产物。 +_FORESHADOW_REVIEW = ForeshadowReview( + planted=[ForeshadowSuggestion(code="F9", title="新埋:山门禁制", where="第2段")], + resolved=[ForeshadowSuggestion(code="F1", title="疑似回收:玉佩", where="第5段")], +) +_PACE_REVIEW = PaceReview( + water=[PaceIssue(where="第3段", reason="信息密度低")], + hook=True, + beat_map=[1, 3, 5, 2], +) + +# 各档位假用量(喂记账;证明各档位调用各自落库)。 +_USAGE = { + "writer": ProviderUsage(input_tokens=11, output_tokens=5), + "analyst": ProviderUsage(input_tokens=23, output_tokens=7), + "light": ProviderUsage(input_tokens=17, output_tokens=9), +} + + +class _FakeMultiTierAdapter: + """实现 `ProviderAdapter` Protocol:按 `req.output_schema` 分支返回 parsed,绝不联网。 + + - `stream()`:吐固定写章 token(writer/draft),末尾喂 writer 用量。 + - `complete()`:据 `req.output_schema` 路由: + `OutlineResult`→大纲(analyst);`ContinuityReview`/`ForeshadowReview`/`PaceReview` + →三审(analyst/light);`ChapterDigestFacts`→验收 digest(light,追溯终稿)。 + 每路带 `ProviderUsage` → 网关据此各落一条 usage_ledger。 + """ + + provider = _PROVIDER + + def capabilities(self) -> Capabilities: + return Capabilities(structured_output=True) + + async def complete(self, req: LlmRequest, model: str) -> ProviderResult: + schema = req.output_schema + if schema is OutlineResult: + return ProviderResult( + text=_OUTLINE_RESULT.model_dump_json(), + parsed=_OUTLINE_RESULT, + usage=_USAGE["analyst"], + ) + if schema is ContinuityReview: + review = ContinuityReview(conflicts=[]) # 零冲突 → 验收 gate 直通 + return ProviderResult( + text=review.model_dump_json(), + parsed=review, + usage=_USAGE["analyst"], + ) + if schema is ForeshadowReview: + return ProviderResult( + text=_FORESHADOW_REVIEW.model_dump_json(), + parsed=_FORESHADOW_REVIEW, + usage=_USAGE["analyst"], + ) + if schema is PaceReview: + return ProviderResult( + text=_PACE_REVIEW.model_dump_json(), + parsed=_PACE_REVIEW, + usage=_USAGE["light"], + ) + # 否则视为 digest 提炼(ChapterDigestFacts):把终稿标记塞进 summary(#4)。 + facts = ChapterDigestFacts( + summary=_FINAL_MARKER, + events=["玉佩浮现"], + locations=["山门"], + ) + return ProviderResult( + text=facts.model_dump_json(), + parsed=facts, + usage=_USAGE["light"], + ) + + async def stream(self, req: LlmRequest, model: str) -> AsyncIterator[StreamChunk]: + for token in _TOKENS: + yield StreamChunk(text=token) + yield StreamChunk(usage=_USAGE["writer"]) + + +@pytest.fixture +async def e2e_sm() -> AsyncIterator[async_sessionmaker[AsyncSession]]: + """真实 DB session 工厂;无 pg 时跳过(每测试清缓存重建 engine、结束 dispose)。""" + get_sessionmaker.cache_clear() + maker = get_sessionmaker() + try: + async with maker() as probe: + await probe.execute(select(1)) + except Exception: + pytest.skip("postgres not reachable") + yield maker + await maker.kw["bind"].dispose() + get_sessionmaker.cache_clear() + + +def _parse_sse(raw: str) -> list[tuple[str, str]]: + """把 text/event-stream 原文解析为 `(event, data)` 帧列表。""" + frames: list[tuple[str, str]] = [] + event: str | None = None + data: str | None = None + for line in raw.splitlines(): + if line.startswith("event:"): + event = line[len("event:") :].strip() + elif line.startswith("data:"): + data = line[len("data:") :].strip() + elif line == "": + if event is not None and data is not None: + frames.append((event, data)) + event, data = None, None + if event is not None and data is not None: + frames.append((event, data)) + return frames + + +def _gateway_override_factory( + adapter: _FakeMultiTierAdapter, +) -> Callable[[AsyncSession], Gateway]: + """构造依赖覆盖:真实 Gateway + 给定假适配器 + 真实 ledger(请求 session)。 + + ledger 用**请求 session**(`Depends(get_session)`,FastAPI 按请求缓存,与端点/验收 + 事务同一实例)→ 由端点流末 / 验收事务的 `commit()` 落库(同 M1/M2 回归校验)。 + """ + + def _override(session: Annotated[AsyncSession, Depends(get_session)]) -> Gateway: + return Gateway( + adapters={_PROVIDER: adapter}, + ledger=SqlAlchemyLedgerSink(session), + resolver=resolve_route, + ) + + return _override + + +async def _cleanup(e2e_sm: async_sessionmaker[AsyncSession], project_uuid: uuid.UUID) -> None: + """按 FK 顺序清理(无级联的先删,再删 project;多数子表经 FK CASCADE)。""" + async with e2e_sm() as cleanup: + await cleanup.execute(delete(UsageLedger).where(UsageLedger.project_id == project_uuid)) + await cleanup.execute(delete(ChapterDigest).where(ChapterDigest.project_id == project_uuid)) + await cleanup.execute(delete(ChapterReview).where(ChapterReview.project_id == project_uuid)) + await cleanup.execute(delete(Outline).where(Outline.project_id == project_uuid)) + await cleanup.execute(delete(Foreshadow).where(Foreshadow.project_id == project_uuid)) + await cleanup.execute(delete(Chapter).where(Chapter.project_id == project_uuid)) + await cleanup.execute(delete(Project).where(Project.id == project_uuid)) + await cleanup.commit() + + +async def test_m3_foreshadow_plant_progress_overdue_after_accept( + e2e_sm: async_sessionmaker[AsyncSession], +) -> None: + """用例 1:伏笔 埋设→进展→验收后到期扫描置 OVERDUE(BackgroundTask)。""" + from ww_api.main import create_app + from ww_api.services.project_deps import ( + get_digest_gateway, + get_review_gateway, + get_session_factory, + get_writer_gateway, + ) + + adapter = _FakeMultiTierAdapter() + override = _gateway_override_factory(adapter) + + app = create_app() + app.dependency_overrides[get_writer_gateway] = override + app.dependency_overrides[get_review_gateway] = override + app.dependency_overrides[get_digest_gateway] = override + # 到期扫描 BackgroundTask 的独立 session 工厂 → 注入真实 sessionmaker(同测试 engine/loop), + # 否则默认 `get_sessionmaker()` 会另建 engine 绑别的 loop(跨 loop 坑)。 + app.dependency_overrides[get_session_factory] = lambda: e2e_sm + + transport = httpx.ASGITransport(app=app) + project_uuid: uuid.UUID | None = None + try: + async with LifespanManager(app): + async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: + # 1) 立项 → 201。 + create_resp = await client.post( + "/projects", + json={"title": "M3 伏笔逾期验证作品", "genre": "玄幻"}, + ) + assert create_resp.status_code == 201 + project_id = create_resp.json()["id"] + project_uuid = uuid.UUID(project_id) + + # 2) 埋伏笔 F1(expected_close_to=2 → 验收第5章后应逾期)→ 201, status=OPEN。 + f1_resp = await client.post( + f"/projects/{project_id}/foreshadow", + json={ + "code": "F1", + "title": "神秘玉佩", + "planted_at": 1, + "expected_close_to": 2, + }, + ) + assert f1_resp.status_code == 201 + assert f1_resp.json()["status"] == "OPEN" + + # 对照:F2(expected_close_to=9 → 不越界,不应逾期)。 + f2_resp = await client.post( + f"/projects/{project_id}/foreshadow", + json={"code": "F2", "title": "远期线索", "expected_close_to": 9}, + ) + assert f2_resp.status_code == 201 + + # 3) PATCH F1 追加一条进展 → 200, progress 追加。 + progress_entry = {"chapter": 3, "note": "玉佩发热"} + patch_resp = await client.patch( + f"/projects/{project_id}/foreshadow/F1", + json={"progress_entry": progress_entry}, + ) + assert patch_resp.status_code == 200 + assert patch_resp.json()["progress"][-1] == progress_entry + + # 4) 写第5章草稿(SSE)+ 自动保存。 + draft_resp = await client.post(f"/projects/{project_id}/chapters/5/draft") + assert draft_resp.status_code == 200 + draft_text = "".join( + json.loads(d)["text"] + for (ev, d) in _parse_sse(draft_resp.text) + if ev == "token" + ) + assert draft_text == "".join(_TOKENS) + save_resp = await client.put( + f"/projects/{project_id}/chapters/5/draft", + json={"text": draft_text}, + ) + assert save_resp.status_code == 200 + + # 5) 验收第5章(无审稿留痕 → 零冲突直通;final_text 含终稿标记 #4)。 + final_text = draft_text + "\n\n" + _FINAL_MARKER + accept_resp = await client.post( + f"/projects/{project_id}/chapters/5/accept", + json={"final_text": final_text, "decisions": []}, + ) + assert accept_resp.status_code == 200 + acc = accept_resp.json() + assert acc["digest_added"] is True + # BackgroundTask 到期扫描在此请求 ASGI 协程内(response 后)跑完才返回。 + + # 6a) 端点视角:GET ?status=OVERDUE 含 F1(status==OVERDUE),不含 F2。 + board_resp = await client.get( + f"/projects/{project_id}/foreshadow", params={"status": "OVERDUE"} + ) + assert board_resp.status_code == 200 + overdue = board_resp.json()["foreshadow"] + overdue_codes = {f["code"] for f in overdue} + assert "F1" in overdue_codes + assert "F2" not in overdue_codes + f1_board = next(f for f in overdue if f["code"] == "F1") + assert f1_board["status"] == "OVERDUE" + + # 6b) DB 真源断言(经 e2e session)。 + assert project_uuid is not None + async with e2e_sm() as verify: + fore_rows = ( + ( + await verify.execute( + select(Foreshadow) + .where(Foreshadow.project_id == project_uuid) + .order_by(Foreshadow.code) + ) + ) + .scalars() + .all() + ) + by_code = {r.code: r for r in fore_rows} + # F1:验收第5章 > expected_close_to(2) → OVERDUE;progress 含那条进展。 + assert by_code["F1"].status == "OVERDUE" + assert progress_entry in by_code["F1"].progress + # F2:expected_close_to(9) 未越界 → 仍 OPEN(对照,扫描不误伤)。 + assert by_code["F2"].status == "OPEN" + + # 终稿 digest 落库且 summary 含终稿标记(#4:从终稿非草稿提炼)。 + digest = ( + await verify.execute( + select(ChapterDigest).where(ChapterDigest.project_id == project_uuid) + ) + ).scalar_one() + assert digest.facts["summary"] == _FINAL_MARKER + assert _FINAL_MARKER not in draft_text + finally: + if project_uuid is not None: + await _cleanup(e2e_sm, project_uuid) + + +async def test_m3_outline_with_foreshadow_windows_upsert_idempotent( + e2e_sm: async_sessionmaker[AsyncSession], +) -> None: + """用例 2:排大纲含伏笔回收窗口;逐章落 `outline`;再次 POST 幂等覆盖;记账落库。""" + from ww_api.main import create_app + from ww_api.services.project_deps import get_outline_gateway + + adapter = _FakeMultiTierAdapter() + override = _gateway_override_factory(adapter) + + app = create_app() + app.dependency_overrides[get_outline_gateway] = override + + transport = httpx.ASGITransport(app=app) + project_uuid: uuid.UUID | None = None + try: + async with LifespanManager(app): + async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: + create_resp = await client.post("/projects", json={"title": "M3 大纲窗口验证作品"}) + assert create_resp.status_code == 201 + project_id = create_resp.json()["id"] + project_uuid = uuid.UUID(project_id) + + # 1) 生成大纲(analyst 假适配器产 OutlineResult)→ 200 OutlineResponse。 + outline_resp = await client.post( + f"/projects/{project_id}/outline", json={"volume": 1} + ) + assert outline_resp.status_code == 200 + chapters = outline_resp.json()["chapters"] + assert len(chapters) == 2 + ch1 = next(c for c in chapters if c["no"] == 1) + assert ch1["volume"] == 1 + assert ch1["beats"] == ["开端:少年入门", "埋线:神秘玉佩"] + assert ch1["foreshadow_windows"][0]["code"] == "F1" + assert ch1["foreshadow_windows"][0]["expected_close_to"] == 2 + + # 2) 再次 POST → 唯一 (project_id, chapter_no) 覆盖,行数不堆叠。 + outline_resp2 = await client.post( + f"/projects/{project_id}/outline", json={"volume": 1} + ) + assert outline_resp2.status_code == 200 + + # DB 真源断言。 + assert project_uuid is not None + async with e2e_sm() as verify: + outline_rows = ( + ( + await verify.execute( + select(Outline) + .where(Outline.project_id == project_uuid) + .order_by(Outline.chapter_no) + ) + ) + .scalars() + .all() + ) + # 幂等:两次 POST 仍是 2 行(每章一行,覆盖非堆叠)。 + assert len(outline_rows) == 2 + row1 = outline_rows[0] + assert row1.chapter_no == 1 + assert row1.volume == 1 + # beats 列形 `{"beats":[...]}`(写侧包裹,见 contracts C3 扩 T3.5)。 + assert row1.beats == {"beats": ["开端:少年入门", "埋线:神秘玉佩"]} + # foreshadow_windows JSONB list 含窗口(code=F1)。 + assert isinstance(row1.foreshadow_windows, list) + assert row1.foreshadow_windows[0]["code"] == "F1" + assert row1.foreshadow_windows[0]["expected_close_to"] == 2 + # 章 2 无窗口。 + assert outline_rows[1].chapter_no == 2 + assert outline_rows[1].foreshadow_windows == [] + + # usage_ledger:两次大纲生成各 1 条 analyst 调用记账(端点 commit 后落库)。 + ledger_rows = ( + ( + await verify.execute( + select(UsageLedger).where(UsageLedger.project_id == project_uuid) + ) + ) + .scalars() + .all() + ) + assert len(ledger_rows) == 2 + assert all(r.input_tokens == _USAGE["analyst"].input_tokens for r in ledger_rows) + assert all(r.provider == _PROVIDER for r in ledger_rows) + finally: + if project_uuid is not None: + await _cleanup(e2e_sm, project_uuid) + + +async def test_m3_three_reviewers_sse_and_review_trace( + e2e_sm: async_sessionmaker[AsyncSession], +) -> None: + """用例 3:三审 SSE(continuity/foreshadow/pace 三审齐)+ 留痕列(foreshadow_sug/pace)真落库。 + + 三审在同一 LangGraph superstep 并行跑、共用请求 session 记账(T3.8 把 + `SqlAlchemyLedgerSink.record` 改 add-only:`session.add` 同步不让步 → 并行协程不交错, + 持久化靠端点 commit)。故三审齐现真持久化,断言严格正向: + - SSE `section` 帧含 continuity/foreshadow/pace 三审,且无 `error` 帧; + - SSE 至少各一条 `foreshadow` / `pace` 事件(foreshadow/pace 真发事件); + - `GET .../reviews` 留痕行 `foreshadow_sug`(list 非空) / `pace`(dict 非空) 列已填; + - DB 真源 `chapter_reviews.foreshadow_sug`/`pace` 列真落库(三审都 done,无静默丢失)。 + """ + from ww_api.main import create_app + from ww_api.services.project_deps import ( + get_review_gateway, + get_writer_gateway, + ) + + adapter = _FakeMultiTierAdapter() + override = _gateway_override_factory(adapter) + + app = create_app() + app.dependency_overrides[get_writer_gateway] = override + app.dependency_overrides[get_review_gateway] = override + + transport = httpx.ASGITransport(app=app) + project_uuid: uuid.UUID | None = None + try: + async with LifespanManager(app): + async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: + create_resp = await client.post("/projects", json={"title": "M3 三审验证作品"}) + assert create_resp.status_code == 201 + project_id = create_resp.json()["id"] + project_uuid = uuid.UUID(project_id) + + # 写第1章草稿 + 自动保存(供续审回退已存草稿)。 + draft_resp = await client.post(f"/projects/{project_id}/chapters/1/draft") + draft_text = "".join( + json.loads(d)["text"] + for (ev, d) in _parse_sse(draft_resp.text) + if ev == "token" + ) + await client.put( + f"/projects/{project_id}/chapters/1/draft", + json={"text": draft_text}, + ) + + # 续审(SSE):三审 section + ≥1 foreshadow/pace 事件、无 error。 + review_resp = await client.post( + f"/projects/{project_id}/chapters/1/review", + json={"draft": draft_text}, + ) + assert review_resp.status_code == 200 + assert review_resp.headers["content-type"].startswith("text/event-stream") + rframes = _parse_sse(review_resp.text) + assert [d for (ev, d) in rframes if ev == "error"] == [] + section_names = { + json.loads(d).get("name") for (ev, d) in rframes if ev == "section" + } + assert {"continuity", "foreshadow", "pace"} <= section_names + foreshadow_events = [d for (ev, d) in rframes if ev == "foreshadow"] + pace_events = [d for (ev, d) in rframes if ev == "pace"] + assert len(foreshadow_events) >= 1 + assert len(pace_events) >= 1 + + # GET 历史:留痕行 foreshadow_sug(list) / pace(dict) 列已填。 + hist_resp = await client.get(f"/projects/{project_id}/chapters/1/reviews") + assert hist_resp.status_code == 200 + latest = hist_resp.json()["reviews"][0] + assert isinstance(latest["foreshadow_sug"], list) + assert len(latest["foreshadow_sug"]) >= 1 + assert isinstance(latest["pace"], dict) + assert latest["pace"]["hook"] is True + + # DB 真源断言:chapter_reviews 留痕列已填。 + assert project_uuid is not None + async with e2e_sm() as verify: + review_rows = ( + ( + await verify.execute( + select(ChapterReview).where(ChapterReview.project_id == project_uuid) + ) + ) + .scalars() + .all() + ) + assert len(review_rows) == 1 + row = review_rows[0] + assert isinstance(row.foreshadow_sug, list) + assert len(row.foreshadow_sug) >= 1 + # 扁平后每条带 kind:"planted"|"resolved"(见 collect 列映射)。 + assert {s.get("kind") for s in row.foreshadow_sug} <= {"planted", "resolved"} + assert isinstance(row.pace, dict) + assert row.pace["hook"] is True + assert row.pace["beat_map"] == [1, 3, 5, 2] + finally: + if project_uuid is not None: + await _cleanup(e2e_sm, project_uuid)