第一年 C4 年终链实施切片 v1
实施状态:
implemented / engineering_verified / normal_gameapp_day360_fixture_blocked
1. 目的与边界
C4 只把既有的 Day 355 来年方针、Day 360 跨年落款和岁末胜利/失败结论收束成一段玩家可读摘要。它不改变事件顺序、四维阈值、存档结构或第二年内容。
本切片只允许复用现有 storyProgress.completed、storyProgress.missed、storyProgress.flags 和 storyProgress.year1Closed。不得新增 GameState、Year1StoryProgress、存档版本或本地存储字段;不得改动 src/data/eventTables/storyline-year1.ts、任何剧情月本、资产、docs/handoff/**。
现有事实:
- Day 355 的
storyline_y1_m12_n05已写入三种来年方针 flag:扩建、稳守、有人味。 - Day 360 的
storyline_y1_m12_n06选择完成后,completeYear1StoryEvent()已把完成记录写入completed并将year1Closed置为true。 evaluateYearEndOutcome()已以 Day 360 夜间、四维 AND 条件和year1CloseoutCompleted门控给出pending / victory / failure / existing_failure。AppOverlayLayer已有四个终局承载块:胜利结算、败局弹窗、败局复盘、时间点存档。C4 不增加第五个 overlay、弹窗、Tab 或存档浏览器入口。
2. 审计结论
2.1 能否完全避开 src/App.tsx 与 useEndgameFlowController.ts
不能。
当前 GameApp 只向 useEndgameFlowController() 传递 storyProgress.year1Closed。控制器据此计算四维结果、写入胜利/失败结算并产出 VictorySettlementPlan、失败记录;它没有 completed、missed、flags,因此无法从 Day 355 的真实选择派生摘要。若绕过控制器直接读取 localStorage,摘要会脱离当前内存状态、破坏纯逻辑边界,也会在未落盘选择后出现错误结论。
最小且必要的接入为:
src/App.tsx仅把已有storyProgress对象追加到useEndgameFlowController({ game }),不新增 state、不改 choice handler、不改 overlay 结构。src/components/endgame/useEndgameFlowController.ts在已有useMemo终局派生链内生成摘要,并把它传给已有胜利计划和已有岁末失败记录。
其余 C4 代码可避开 AppOverlayLayer.tsx、VictorySettlementPanel.tsx、GameOverModal.tsx、EndgameRecoveryPanel.tsx、SaveSnapshotBrowser.tsx:胜利摘要写进既有 annual_ledger 块,失败摘要写进既有 FailureEventRecord.summary,现有四块会自然消费它们。
2.2 既有四块的复用位置
| 既有承载块 | C4 行为 | 禁止事项 |
| --- | --- | --- |
| VictorySettlementPanel | 仅扩充已有 annual_ledger section 的玩家详情,展示来年方针和跨年落款。 | 不新增 VictorySettlementSectionId,不创建第五张结算卡。 |
| GameOverModal | 继续消费 terminalFailureDisplay.description;岁末失败记录摘要带入现有 reasonDesc。 | 不新增失败弹窗。 |
| EndgameRecoveryPanel | 继续消费首条失败记录和既有“岁末四维复盘”。 | 不添加独立 C4 复盘块。 |
| SaveSnapshotBrowser | 继续显示现有终局快照摘要。 | 不新增 C4 专用存档类型、tag 或浏览入口。 |
3. 最小三层实现合同
3.1 数据层
新增 src/data/endgame/year1CloseoutSummary.ts,只放 type 与原始静态配置,不放函数。
export type Year1NextYearFocusId = 'bigger' | 'steadier' | 'warmer' | 'unresolved';
export interface Year1CloseoutCopy {
readonly title: string;
readonly annualLedgerDetail: string;
readonly failureLead: string;
}
export interface Year1CloseoutSummary {
readonly focus: Year1NextYearFocusId;
readonly title: string;
readonly annualLedgerDetail: string;
readonly failureLead: string;
}
export const YEAR1_ANNUAL_DIRECTION_EVENT_ID = 'storyline_y1_m12_n05';
export const YEAR1_CLOSEOUT_EVENT_ID = 'storyline_y1_m12_n06';
export const YEAR1_NEXT_YEAR_FOCUS_FLAGS: readonly { readonly focus: Exclude<Year1NextYearFocusId, 'unresolved'>; readonly flag: string }[] = [...];
export const YEAR1_CLOSEOUT_COPY: Readonly<Record<Year1NextYearFocusId, Year1CloseoutCopy>> = {...};
玩家文案只能来自 YEAR1_CLOSEOUT_COPY;flag、event id、choice id 不得进入 JSX、终局记录标题或 snapshot note。
3.2 逻辑层
新增 src/utils/endgame/year1CloseoutSummary.ts,只放纯函数,依赖数据层和既有 Year1StoryProgress type。
export const deriveYear1NextYearFocus = (
progress: Pick<Year1StoryProgress, 'completed' | 'missed' | 'flags' | 'year1Closed'>,
): Year1NextYearFocusId;
export const buildYear1CloseoutSummary = (
progress: Pick<Year1StoryProgress, 'completed' | 'missed' | 'flags' | 'year1Closed'>,
): Year1CloseoutSummary;
export const buildYearEndFailureSummary = (
closeout: Year1CloseoutSummary,
evaluation: Pick<YearEndOutcomeEvaluation, 'summary' | 'unmetDimensions'>,
): string;
规则:
year1Closed是硬门;为false时只返回中性未落款文案,且控制器不得结算。- Day 355 在
missed中,或没有完成记录及可识别 flag 时,使用unresolved,不虚构玩家来年方针。 - 正常路径以
flags识别方针,并以 Day 355 的completed记录校验其已发生;异常旧档出现多个 flag 时按YEAR1_NEXT_YEAR_FOCUS_FLAGS固定顺序取第一项,并由回归覆盖。 - Day 360 的
completed和year1Closed只用于确认既有落款事实,不改变completeYear1StoryEvent()、不补写 flag、不推进 Year 2。 buildYearEndFailureSummary()必须保留evaluation.summary的未达线信息;C4 只增加落款与方针上下文,不能掩盖失败原因。
修改 src/data/endgame/victorySettlement.ts:将 Year1CloseoutSummary 作为 VictorySettlementPlanInput 的新增必填输入;该文件只扩充 type import 和数据合同,不放派生逻辑。修改 src/utils/endgame/victorySettlement.ts:仅把 annualLedgerDetail 追加进已有 annual_ledger.details。不得新增 section id。
修改 src/utils/endgame/failureOutcome.ts:为 applyYearEndFailureOutcome() 增加可选的 C4 玩家摘要参数,写入已有 FailureEventRecord.summary;未传入时保持当前文本和既有调用兼容。不得改变四维计算、优先级、失败 ID 或终局状态机。
3.3 UI 与接线层
src/components/endgame/useEndgameFlowController.ts:
EndgameGameContext增加storyProgress: Year1StoryProgress,只读使用。- 现有
yearEndOutcome保持以storyProgress.year1Closed作为落款门。 - 新增 memo:
const year1CloseoutSummary = buildYear1CloseoutSummary(game.storyProgress)。 - 传该摘要给已有
buildVictorySettlementPlan();失败 effect 调用已有applyYearEndFailureOutcome()时传入其玩家失败摘要。 - 不新增 React state、effect、overlay 开关或存档写入路径。
src/App.tsx:在现有 useEndgameFlowController({ game: { ... } }) 调用中增加一行 storyProgress。这是唯一允许的 App.tsx 改动。
UI 组件无业务计算:VictorySettlementPanel.tsx 只渲染计划已有 annual_ledger.details;GameOverModal.tsx 和 EndgameRecoveryPanel.tsx 继续渲染既有 props。C4 不创建组件。
4. 精确改动文件与测试
| 类别 | 文件 | 操作 |
| --- | --- | --- |
| 数据 | src/data/endgame/year1CloseoutSummary.ts | 新增纯 type、ID/flag 映射和玩家文案。 |
| 数据 | src/data/endgame/victorySettlement.ts | 为既有 VictorySettlementPlanInput 增加摘要 type 合同。 |
| 逻辑 | src/utils/endgame/year1CloseoutSummary.ts | 新增纯派生函数。 |
| 逻辑 | src/utils/endgame/victorySettlement.ts | 将摘要折入现有年度总账 details。 |
| 逻辑 | src/utils/endgame/failureOutcome.ts | 将可选 C4 摘要折入既有岁末失败记录。 |
| 接线 | src/components/endgame/useEndgameFlowController.ts | 消费已有进度并向既有结算函数传值。 |
| 入口 | src/App.tsx | 仅传递已有 storyProgress。 |
| 回归 | src/utils/endgame/year1CloseoutSummary.regression.ts | 新增纯逻辑回归。 |
| 回归 | src/utils/endgame/victorySettlement.regression.ts | 断言摘要只落在 annual_ledger,section 数量仍为四。 |
| 回归 | src/utils/endgame/failureOutcome.regression.ts | 断言失败摘要保留未达线原因、pending 不写记录、重复调用幂等。 |
明确不改:src/types.ts、保存/迁移文件、src/data/eventTables/storyline-year1.ts、所有 docs/story/**、public/**、pic/**、docs/handoff/**。
5. 验收
5.1 自动化
在实现后依次执行:
npx tsx --test src/utils/endgame/year1CloseoutSummary.regression.ts src/utils/endgame/victorySettlement.regression.ts src/utils/endgame/failureOutcome.regression.ts
npm run test:year1
npm run lint
npm run build -- --emptyOutDir false
git diff --check
test:year1 当前不覆盖终局回归,不能替代第一条显式命令。若 lint 仍报现有基线错误,必须逐项确认本切片文件没有新增诊断,并如实记录基线噪声。
5.2 正常 GameApp Day360 浏览器验收
使用 npm run dev 后打开 http://127.0.0.1:3106/,URL 不得含 record=year1;录制模式只能用于剧情画面预览,不能作为本验收证据。使用现有、明确标记为测试用的 Day360 存档或受控加速过程,且它必须走正常 GameApp 的事件选择、存档与 overlay 管线。
- 胜利夹具:Day355 已完成且分别覆盖三种方针,Day360 夜间尚未落款,四维为 10000 / 70 / 80 / 360。完成跨年选择后,先关闭剧情,再只出现既有胜利结算;年度总账内出现对应的玩家方针文案,结算仍只有四个 section。
- 失败夹具:同样完成 Day355 与 Day360,但至少一项四维未达线。完成跨年选择后,先出现既有败局入口;败局描述同时包含玩家方针上下文和真实未达线原因,复盘页没有新 C4 卡片。
- pending 夹具:Day360 夜间四维任意,但不完成跨年选择。不得弹胜利、不得写岁末失败记录、不得写终局快照;剧情选择完成后才允许进入第 1 或第 2 步。
- 刷新夹具:在胜利弹窗、失败弹窗和失败复盘各刷新一次。现有终局快照、周目结算与失败记录均不得重复;Day355 结果仍能从已有
storyProgress复原。 - 每条路径检查 DOM 与可见文本:不得出现
next_year_focus_、事件 ID、choice ID、completed、flags、year1Closed或其他开发字段。
浏览器证据应记录测试夹具来源、窗口尺寸、路径结果和截图;加速/测试存档不得伪装为自然 360 天游玩。
6. 风险与回退
| 风险 | 处理 | 回退 |
| --- | --- | --- |
| Day355 被 defer/miss,Day360 fallback 仍可落款 | unresolved 走中性玩家文案,绝不编造方针。 | 仅撤回 C4 摘要派生,既有 Day360 fallback 保持。 |
| 旧档有多个或残缺方针 flag | 固定顺序、完成记录与回归共同保证确定性。 | 回退到 unresolved,不迁移存档。 |
| 摘要覆盖失败原因 | buildYearEndFailureSummary() 必须包含 evaluation.summary;失败回归锁定。 | 恢复原 FailureEventRecord.summary,四维结算不受影响。 |
| 终局重复结算 | 不新增 effect 或写入路径,复用控制器已有 replay guard。 | 回退新增摘要参数,不碰现有 guard。 |
| 范围漂移到 Year2 或剧情改写 | C4 只显示已选择的来年语气,不消费方针为数值或内容。 | 删除 C4 数据/逻辑接线;不需要数据迁移。 |
现有《第一年功能模块打磨总纲》仍写有“当前模块不消费 Day355 来年方针”。C4 是对该限制的最小显示性例外:只在既有终局文本中回看已存偏好,不把偏好接入 Year2 机制。实现前应确认该例外已获 Owner 接受。
7. 派工边界
建议单一端到端实现 lane 负责上表十个源码/回归文件,避免 App.tsx、终局控制器与结算逻辑被多 lane 同时改写。其余并行 lane 只能做只读浏览器验收或审阅,不得改 C4 文件。完成条件是自动化、正常 GameApp 四路径浏览器证据和定向 git diff --check 全部具备;不等同于提交、推送、发布或 Owner 验收。
8. 2026-07-27 实际落地补遗
本节记录实际实现,并取代第 3、4 节中只覆盖“来年方针一句话”的建议命名;历史审计判断保留作过程依据。
实际三层落点:
| 层 | 文件 | 已落地内容 |
| --- | --- | --- |
| 数据 | src/data/storyEvents/year1Review.ts | 四幕选择映射、经营风格句库、Day 355 方针映射、关系阶段玩家文案;无函数。 |
| 逻辑 | src/utils/storyEvents/year1ReviewLogic.ts | 从既有 completed / missed / flags / year1Closed 与 bondStates 派生年度复盘;不新增 schema。 |
| UI 接线 | src/components/endgame/useEndgameFlowController.ts、src/App.tsx | 控制器消费派生结果;App.tsx 只增加 storyProgress 参数透传。 |
现有终局四块的实际消费:
年度总账:显示经营风格,并列出开张、扩张、团队、高光四幕的真实选择;缺失事件不伪造。关系快照:显示岁末排序最高的两条现有羁绊及阶段。名声定格:保留既有数值与语义,不额外塞入重复信息。江湖阅历折算预览:追加 Day 355 来年方针;Day 355 miss 时显示“尚未定下”。败局复盘:保留真实未达线原因,在现有记录摘要与 impacts 中复用同一年度复盘,不新增失败系统或 UI 卡片。
回归已纳入 npm run test:year1:
src/utils/storyEvents/year1ReviewLogic.regression.tssrc/utils/endgame/victorySettlement.regression.tssrc/utils/endgame/failureOutcome.regression.ts
当前证据:
- 聚焦 C4 / 终局回归:
27/27。 npm run test:year1:92/92。npm run test:w29:47/47。npm run lint:通过。npm run build -- --emptyOutDir false:通过,仅保留既有运行时资源与大 chunk 警告。- 正常 GameApp
http://127.0.0.1:3106/:可进入当前周目,URL 不含record=year1,录制根节点不存在,内部字段未泄露,控制台错误0。 - Day 360 胜利 / 失败 / pending / 刷新四路径:仓库与当前 UI 没有可丢弃 Day 360 测试夹具,仍为
browser_verification_blocked_by_fixture,不得写成已验收。 - 场景资产:Day 350、355、360 正式场景路径仍缺文件;该缺口不由本逻辑切片伪装为完成。
复盘完整性规则:正常 Year 1 至少需要三个不同幕的真实完成选择才判为“可归纳经营风格”;不足时保留已存在的选择,经营风格降级为“尚不足以归纳”,并显示“关键选择记录不足,未虚构缺失经历”。completed.choiceId 是玩家选择主真值,方针 flag 只作旧档兼容回退;不得为了凑满三条而把 missed 写成已选。