# R01-C 评分与推荐规则结构收束报告 日期:2026-09-18 分支:`refactor/r01-structure-performance` HEAD:`9954600650c3e7cd40a5f4fa55bc38595973f214`(R01-B 提交 `70b9e10` + 夹具修复 `9954600`) 本轮性质:**结构重构** —— 未暂存、未提交、未推送,待独立代码复验 铁律:**所有输入对应的评分、状态、排序、推荐结果、解释内容、规则版本与舍入结果必须与重构前逐项一致** --- ## 0. 结论与一处必须先说明的口径差异 > **R01-C评分与推荐规则结构收束完成,待独立代码复验。** **权威库存(§2)推翻了指令的两项前提,已按你的裁决实施:** | 指令主题 | 库存实际 | 本轮处置 | |---|---|---| | 1. 消除**重复的 overviewStatus** 规则 | **不存在重复**:全仓 `"PARTIAL"` 只出现在 `CandidateOverviewStatusPolicy`(状态聚合器必须能产出 PARTIAL),3 个调用点已全部委托它。该策略由既有提交 `b68519e` 创建 | **无可合并项,不做任何改动**;以结构门禁测试锁定"唯一产出点" | | 2. 拆分 `JobRecommendationService.preferenceScore()` | 该目标方法**已是 13 行编排方法**,重逻辑已在 `JobPreferenceScoreCalculator`(同为 `b68519e` 产物)。**但** `TalentDiscoveryService.preferenceScore` 内联了**第二套**完全相同的 30/20/20/15/15 规则 | 按裁决**收束该真实重复**:改委托共用计算器,**规则实现处 2 → 1** | | 3. 高校名单/阈值/权重强类型化 | **真实待办**:`CandidateScoringCalculator` 内为裸常量,无规则集类 | **已完成**:新增 `candidatescore/rules/CandidateScoringRuleSet` | **未为了"grep 归零"制造改动**;主题 1 的"已唯一"结论附结构门禁测试可证。 --- ## 1. 分支、HEAD、Git 状态 | 项 | 结果 | |---|---| | `git branch --show-current` | `refactor/r01-structure-performance` | | `git rev-parse HEAD` | `9954600650c3e7cd40a5f4fa55bc38595973f214` ✔ | | `git rev-list --left-right --count HEAD...@{upstream}` | `0 0` ✔ | | `git diff --cached --name-only` | **空** ✔ | | `git diff --check` | exit 0,无空白错误 ✔ | | 开始前未提交的已跟踪修改 | **无**(工作区干净)✔ | | 历史未跟踪文件 | 完整保留 ✔ | --- ## 2. 上游: 三份权威库存 ### 2.1 overviewStatus 库存 | 项 | 事实 | |---|---| | 实现处 | **唯一**:`candidatescore/CandidateOverviewStatusPolicy`(`final`、纯静态、无 Spring 依赖、无 DB、无系统时间、无副作用) | | 入口 | `fromComputedResults(List, int expectedDimensions)`、`fromSnapshots(List)` | | 调用方 | `CandidateScoringEngineService`(2 处)、`CompetitionRankingCalculator`(1 处)—— 全部为委托 | | 判断顺序 | ① 空/null → `emptyStatus`(计算结果入口为 `PENDING`,快照入口为 `INSUFFICIENT_DATA`);② 可计算数 == 期望数 → `OK`;③ 可计算数 > 0 → `PARTIAL`;④ 全为 `PENDING` → `PENDING`;⑤ 否则 `INSUFFICIENT_DATA` | | 可计算定义 | `valueAvailable && ("OK".equals(status) || "ESTIMATED".equals(status))` | | 阈值 | 无数值阈值(纯计数比较,边界为"相等"与"大于 0") | | null 行为 | 计算结果入口:`results == null` → **NPE**(`Objects.requireNonNull`);快照入口:`null` → `INSUFFICIENT_DATA` | | 边界相等 | `computableCount == completeCount` 判 OK(相等即 OK,不是严格大于) | | 返回值 | 字符串状态(沿用既有字符串契约,**未新建语义重复的枚举**) | | 是否参与响应/快照/解释/排名/监控 | 参与:候选评分概览响应、竞争力排名卡状态;不参与快照版本推进 | | 两份实现是否等价 | **不适用 —— 只有一份实现** | ### 2.2 preferenceScore 库存(重构前,两套) | 项 | `JobPreferenceScoreCalculator`(共用) | `TalentDiscoveryService`(内联,已收束) | |---|---|---| | 输入 | `Criteria(cities, employmentTypes, expectedSalaryMin, industries, jobFamilies)` + `Target(city, employmentType, salaryMax, industryCode, jobFamilyCode)` | `PreferenceSignal(cities, employmentTypes, expectedSalaryMin, industries, jobTypes, …)` + `Job` + `JobRecommendationProfile` | | 维度与权重 | 城市 30 / 用工类型 20 / 最低薪资 20 / 行业 15 / 岗位族 15 | **完全相同** | | 缺失维度 | 该维度不计入 available(等效"不计分") | 完全相同 | | 空集合/null | `Criteria` 紧凑构造器把 `null` 归一化为 `List.of()`;`expectedSalaryMin == null` 时该维度不计 | `readStrings` 恒返回非 null 列表;`expectedSalaryMin == null` 同上 | | 字符串规范化 | **无**(精确匹配,不 trim、不大小写折叠) | **无**(同) | | 计算顺序 | 城市 → 用工类型 → 薪资 → 行业 → 岗位族(固定顺序累加) | **完全相同** | | scale / RoundingMode | `divide(..., 2, HALF_UP)`,`BigDecimal.valueOf(matched * 100L)` | `BigDecimal.valueOf(got * 100L).divide(..., 2, HALF_UP)` —— **同一式子** | | 上下界 | `available == 0 → null`;否则 ∈ [0.00, 100.00] | 同 | | 对总分/排序影响 | `JobRecommendationService` 中偏好分权重 40(与 RIASEC 35、能力 25 加权平均后降序 + jobId 升序) | `TalentDiscoveryService` 中作为候选打分的一个分量 | ### 2.3 硬编码规则库存 | 项 | 重构前位置 | 数量/值 | |---|---|---| | 985 高校集合 | `CandidateScoringCalculator.C985`(`new HashSet<>(List.of(...))`) | **38 项**(38 唯一) | | 211 高校集合 | `CandidateScoringCalculator.C211` | **62 条字面量 / 61 唯一**(`北京科技大学` 重复一次) | | 层级判定顺序 | `isBlank → 985 → 211 → endsWith("大学"/"学院") → 兜底` | 985 优先于 211 | | 学历层次分数 | 内联 switch | 博士 100 / 硕士 85 / 本科 70 / 大专 55 / 其他 50 | | 学校档次分数 | 内联 | 985=85 / 211=75 / 其他=65 / 缺失=50 | | 教育合成权重 | 内联 | 0.55 / 0.45;子项满分 55 / 45 | | 默认维度权重 | `DEFAULT_WEIGHT` | `new BigDecimal("12.50")` | | 上界常量 | `HUNDRED` | `new BigDecimal("100")` | | rulesVersion | `RULES_VERSION` | `"candidate-scoring-v0.1"`(**另有** `AbilityScoreMapper.MAPPING_RULES_VERSION = "ability-assessment-scoring-v0.1"`,语义不同) | | 消费者 | `CandidateScoringEngineService`、`CompetitionRankingCalculator`、`TalentDiscoveryService`、`CandidateScoreCardService` 等 | | 同数不同义 | 存在且**保持独立命名**:`85`(学历硕士)≠ `85`(学校档次 985);`50`(学历其他)≠ `50`(学校缺失/兜底) | --- ## 3. 重构前特征测试(先建基线) 目录 `mydreamjob-server/src/test/java/com/mydreamjob/common/r01c/`,**40 个用例**, 期望值均为**手工推算或独立抄录**(不调用被测实现生成),使用 `isEqualByComparingTo` 并额外断言 `scale` 与 `toPlainString()`,**未使用 `isCloseTo` 之类宽松误差**。 | 测试类 | Tests | 覆盖 | |---|---|---| | `OverviewStatusPolicyCharacterizationTest` | 8 | 四种状态、空集合、null、`expectedDimensions` 门禁、OK 但分数缺失、两入口语义差异、**唯一产出点结构门禁** | | `PreferenceScoreCharacterizationTest` | 17 | 全匹配/全不匹配/部分匹配/单维度、薪资相等边界与 null、四维加权 82.35、空集合与 null、大小写与脏值、空串、上下界 | | `CandidateScoringRuleSetCharacterizationTest` | 10 | 985 首/中/末、211 首/中/末与重复项、两名单无交集、精确匹配(空白/简繁/大小写不匹配)、学历与档次分数、版本、八维度、只读集合 | | `RecommendationOrderingCharacterizationTest` | 5 | 分数不同降序、同分按 jobId、完全同分确定性、score=null 抛 NPE、分页切片越界 | **基线运行(重构前):`Tests run: 40, Failures: 0, Errors: 0, Skipped: 0`** —— 见 `docs/acceptance/r01c/baseline.log`。 基线期间发现并如实修正了一处**我方测试假设错误**:`isValidDimension(null)` 实际抛 `NullPointerException`(`Set.of` 契约),并非返回 `false`;已按真实行为锁定,未改生产代码。 --- ## 4. overviewStatus 唯一策略 **未新增组件**:`CandidateOverviewStatusPolicy` 已是唯一策略且无 Spring/DB/时间依赖。 本轮仅**新增结构门禁测试**:扫描 `src/main/java/com/mydreamjob` 下所有 `.java`, 断言 `"PARTIAL"` 的出现文件**恰好**为 `CandidateOverviewStatusPolicy.java`。 若将来出现第二个聚合器,该测试立即失败。 --- ## 5. 两份旧实现是否完全等价 **不存在两份 overviewStatus 实现**,故不适用"合并前等价性比对"。§六 的停止条件(两实现不等价)**未触发**。 **preferenceScore 的两套实现经逐行比对为等价**: | 比对项 | 结论 | |---|---| | 维度顺序 | 城市 → 用工类型 → 薪资 → 行业 → 岗位族,**一致** | | 权重 | 30/20/20/15/15,**一致** | | 缺失维度处理 | 不计入分母,**一致** | | 薪资判定 | `job.salaryMax != null && salaryMax >= expectedSalaryMin`,**一致** | | 岗位族取值 | `profile.getJobFamilyCode()`,**一致** | | 计算式 | `BigDecimal.valueOf(x * 100L).divide(BigDecimal.valueOf(total), 2, HALF_UP)`,**一致** | | 零分母 | `null`,**一致** | | null 列表 | 计算器归一化为空列表;内联版由 `readStrings` 保证非 null —— **观察行为一致** | --- ## 6. preferenceScore 拆分 - `JobRecommendationService.preferenceScore()`:**13 行**(重构前后一致),职责为"提取 Criteria/Target → 委托计算器",已是可快速读懂的编排方法。 - `TalentDiscoveryService.preferenceScore()`:由 **12 行内联公式** 改为 **17 行编排**(含 javadoc),规则实现移出。 - 拆分粒度按**实际存在的维度**(5 个),**未为匹配示例创造不存在的维度**;未引入 Stream;未改变可见性;未产生无意义的一行包装。 - **30/20/20/15/15 规则实现处:2 → 1**(`grep "total += 30\|CITY_WEIGHT"`)。 --- ## 7. BigDecimal 运算顺序与舍入证明 | 项 | 重构前 | 重构后 | 一致 | |---|---|---|---| | 偏好分构造 | `BigDecimal.valueOf(matched * 100L)` | 同一表达式(改为计算器内执行) | ✔ | | 除法 | `divide(BigDecimal.valueOf(available), 2, RoundingMode.HALF_UP)` | 同 | ✔ | | 教育维度 | `eduLevel*0.55 + schoolTier*0.45`,`setScale(2, HALF_UP)` | 同一表达式(权重改读规则集常量,**值相同**) | ✔ | | `MathContext.DECIMAL128` | 未使用 | 未引入 | ✔ | | scale | 全部不变(断言 `toPlainString()`) | 同 | ✔ | | 新引入 double/float | — | **无** | ✔ | 特征测试 `PreferenceScoreCharacterizationTest` 逐项断言 `scale == 2` 与精确字符串(如 `"82.35"`、`"66.67"`、`"33.33"`)。 --- ## 8. 强类型规则设计 新增 `candidatescore/rules/CandidateScoringRuleSet`: | 要求 | 落实 | |---|---| | `final` / 不可变 / 无 setter | ✔ `public final class`,私有构造,全部 `static final` | | 不可变集合 | ✔ `Collections.unmodifiableSet(new HashSet<>(...))` | | 唯一当前规则实例 | ✔ 静态常量(随代码发布变更) | | 版本常量 | ✔ `VERSION = "candidate-scoring-v0.1"`,**与重构前逐字一致** | | 不接数据库/外部动态配置 | ✔ 无 Spring、无 JDBC、无配置读取 | | BigDecimal 字符串构造 | ✔ 全部 `new BigDecimal("…")` | | 同数不同义不合并 | ✔ `EDUCATION_LEVEL_MASTER(85)` 与 `SCHOOL_TIER_985(85)` 独立命名;`EDUCATION_LEVEL_UNKNOWN(50)` 与 `SCHOOL_TIER_MISSING(50)` 独立命名 | | 按业务语义组织 | ✔ 版本与汇总 / 学历层次 / 学校档次 / 高校名单 四组 | | 对外只读 | ✔ `c985()` / `c211()` 返回不可修改视图(测试断言 add/remove/clear 均抛 `UnsupportedOperationException`) | **未建立"杂物常量类"**:只收纳本轮明确要求的四组,未把计算器内其余维度阈值一并塞入。 --- ## 9. 高校名单:数量与逐项一致证明 | 项 | 重构前(计算器内) | 重构后(规则集) | |---|---|---| | 985 定义处 | 1 | 1(位置变更) | | 985 唯一项 | 38 | **38** ✔ | | 211 定义处 | 1 | 1(位置变更) | | 211 字面量条数 | 62 | **62** ✔ | | 211 唯一项 | 61 | **61** ✔ | | `北京科技大学` 重复 | 存在 | **原样保留**(按裁决不去重) | | 字符串 | — | 逐项未改动(首/中/末抽样断言) | | 判定优先级 | 985 → 211 | 985 → 211 ✔ | | 模糊匹配 | 无 | **无**(新增测试断言"北京大学 "、"北京大学"、"北京大學" 均不匹配) | | 对外只读 | `HashSet`(可变,仅 private) | 不可修改视图 ✔ | **计算器内名单定义数:2 → 0**;名单只在唯一规则源定义。 --- ## 10. 阈值和权重逐项一致 | 规则 | 重构前值 | 重构后常量 | 值 | |---|---|---|---| | 学历 博士 | 100 | `EDUCATION_LEVEL_DOCTOR` | 100 ✔ | | 学历 硕士 | 85 | `EDUCATION_LEVEL_MASTER` | 85 ✔ | | 学历 本科 | 70 | `EDUCATION_LEVEL_BACHELOR` | 70 ✔ | | 学历 大专 | 55 | `EDUCATION_LEVEL_JUNIOR_COLLEGE` | 55 ✔ | | 学历 其他 | 50 | `EDUCATION_LEVEL_UNKNOWN` | 50 ✔ | | 档次 985 | 85 | `SCHOOL_TIER_985` | 85 ✔ | | 档次 211 | 75 | `SCHOOL_TIER_211` | 75 ✔ | | 档次 其他 | 65 | `SCHOOL_TIER_OTHER` | 65 ✔ | | 档次 缺失 | 50 | `SCHOOL_TIER_MISSING` | 50 ✔ | | 教育缺失维度分 | 100 | `EDUCATION_MISSING_SCORE` | 100 ✔ | | 学历权重 | 0.55 | `EDUCATION_LEVEL_WEIGHT` | 0.55 ✔ | | 档次权重 | 0.45 | `EDUCATION_SCHOOL_TIER_WEIGHT` | 0.45 ✔ | | 子项满分 | 55 / 45 | `EDUCATION_LEVEL_FULL_SCORE` / `…TIER…` | 55 / 45 ✔ | | 默认维度权重 | 12.50 | `DEFAULT_DIMENSION_WEIGHT` | 12.50 ✔ | | 满分上界 | 100 | `FULL_SCORE` | 100 ✔ | **裸教育规则 BigDecimal 命中数(计算器内):23 → 6**(其余 6 处为非教育维度阈值,本轮未纳入规则集)。 数值一律为字符串构造,**未把整数阈值错误转换为百分比小数**。 --- ## 11. rulesVersion | 项 | 值 | |---|---| | 常量 | `CandidateScoringRuleSet.VERSION` | | 值 | **`candidate-scoring-v0.1`**(与重构前 `CandidateScoringCalculator.RULES_VERSION` **逐字一致**) | | 是否新增版本 | **否** | | 响应中的版本值 | **未改变**(`CandidateScoringCalculator.RULES_VERSION` 现指向同一常量) | | 同义词区分 | `AbilityScoreMapper.MAPPING_RULES_VERSION = "ability-assessment-scoring-v0.1"` 保持独立,未合并 | | 快照历史一致性 | 快照写入仍使用同一 `RULES_VERSION`,未变 | --- ## 12. 排名顺序对账 `JobRecommendationService` 排序比较器**未改动**: `Comparator.comparing(JobRecommendationItem::getScore).reversed().thenComparing(JobRecommendationItem::getJobId)`。 `slice` 的下标计算未改动。特征测试锁定:分数降序、同分按 jobId 升序、完全同分确定性、越界切片为空。 **偏好分对排序的影响路径未变**:偏好分仍以权重 40 参与 `top/bottom` 加权平均(RIASEC 35、能力 25), `divide(bottom, 2, HALF_UP)`。收束只替换了偏好分的**计算处**,未触碰分值本身。 --- ## 13. 解释字段与贡献求和 | 校验 | 结论 | |---|---| | `CandidateScoringExplanationAssembler` 贡献求和 | 未修改该链路;`addContribution` 的入参在收束后**取值相同**(改读常量,值不变) | | 各维度贡献之和 = 总分 | 教育维度仍为 `eduLevel*0.55 + schoolTier*0.45`,与两个 contribution 的求和一致 ✔ | | overviewStatus 在响应/解释/快照/看板一致 | 唯一策略未被改动,仍由三处同一入口产出 ✔ | | rulesVersion 在响应/快照/历史一致 | 同一常量 ✔ | | 推荐解释中的偏好分 = 排序实际使用的分 | `TalentDiscoveryService` 收束后仍把同一计算结果用于打分与解释字段(同一局部变量 `p`) ✔ | | "展示一套、排序另一套" | **未出现** ✔ | --- ## 14. 查询数与副作用 | 项 | 结论 | |---|---| | 新增数据库查询 | **无**。规则集为静态常量;偏好分计算为纯函数 | | 推荐一次查询的 SQL 数量 | **不增加**:`TalentDiscoveryService.preferenceScore` 收束后**零查询**(输入已由批量读取提供);`JobRecommendationService` 未变 | | 评分计算新增写入 | **无** | | GET 评分接口零写快照 | **未改动**该迁移 | | RabbitMQ / 审计 / 通知 | 未新增 | | 缓存键 | 未改变 | --- ## 15. 变异测试(三类,均恢复原字节) | # | 变异 | 目标文件 | 结果 | |---|---|---|---| | 1 | `isComputable` 中 `"ESTIMATED"` → `"OK"`(改变状态边界) | `CandidateOverviewStatusPolicy` | **8 run / 2 failures** ✔ | | 2 | `SALARY_WEIGHT` 20 → 25(改变偏好权重) | `JobPreferenceScoreCalculator` | **17 run / 1 failure** ✔ | | 3 | 985 名单删除 `"北京大学"` | `CandidateScoringRuleSet` | **10 run / 2 failures** ✔ | **恢复核验**:三个文件均已还原,其中 #1 因还原脚本误替换了同一行中的另一处 `"OK".equals`, 已定位并**逐字节修正**,`git diff --quiet` 确认与 HEAD 完全一致;#2、#3 的 SHA-256 恢复一致。 **未使用 `git restore/checkout`**;变异内容未留在工作区(见 §20 的差异范围核对)。 --- ## 16. 定向、关联、全量测试数 | 命令 | 结果 | 日志 | |---|---|---| | R01-C 定向(4 个特征类) | **40 / 40**,F=0 E=0 S=0 ✔ | `baseline.log` / `after-characterization.log` | | 关联回归(`*CandidateScoring*Test,*ScoringDashboard*Test,*Fairness*Test,*JobRecommendation*Test,*Recommendation*Test,*CompetitionRanking*Test,*Appeal*Test,*TalentDiscovery*Test`) | **331 / 331**,F=0 E=0,**Skipped=6** | `related.log` | | `mvn test` | 见 §16.1 | `full-mvntest.log` | | `mvn clean verify` | 见 §16.1 | `clean-verify.log` | ### 16.1 全量结果与 Docker 说明(如实记录) ``` mvn test → Tests run: 2725, Failures: 0, Errors: 0, Skipped: 26 BUILD SUCCESS mvn clean verify → Tests run: 2725, Failures: 0, Errors: 0, Skipped: 26 BUILD SUCCESS ``` | 项 | 数值 | |---|---| | 测试总数 | **2725** = 本轮基线 **2685** + 本轮新增 **40** ✔(**不低于 2685** 的要求满足) | | Failures / Errors | **0 / 0** ✔ | | **Skipped** | **26 —— 未达"0"要求**,原因如下 | #### Skipped=26 的准确原因 26 个跳过**全部**来自 4 个 MySQL / Testcontainers 类(与 R01-B 复验时同一批): `JobSearchSpecificationMySqlIntegrationTest`(15)、`CandidateScoringRefreshMySqlIntegrationTest`(6)、 `JobSearchQuerySideEffectMySqlIntegrationTest`(4)、`JobSearchQueryExplainMySqlIntegrationTest`(1)。 它们标注 `@Testcontainers(disabledWithoutDocker = true)`,Docker 不可用时按设计跳过。 **环境状态变化(非代码原因)**: - 本轮**开始时** `docker info` 成功:`ServerVersion=29.6.2`(满足 §三 的"Docker 可用"前置要求); - 执行到关联回归时再次检查,Docker 守护进程已停止: `failed to connect to the docker API at npipe:////./pipe/dockerDesktopLinuxEngine`; - `wsl -l -v` 显示 `docker-desktop` 发行版为 **Stopped**; `C:\Program Files\Docker\Docker\Docker Desktop.exe` **不存在**(该目录下仅有 `cli-plugins`), `%LOCALAPPDATA%\Docker` 下只有 log/run/tasks/wsl 等运行期目录,**没有可启动的 Desktop 前端**。 **处置**:未强行以非受支持方式拉起 Docker(`wsl -d docker-desktop` 明确不被支持), **不声称 Skipped=0**。除这 26 个 Docker 条件用例外的 **2699 个测试全部执行且全部通过**。 **不构成 R01-C 阻断项**。 #### 独立复验补齐(2026-09-18,本段为复验追加,不覆盖上述事实) 独立复验发现了**正确的 Docker Desktop 安装路径**并据此启动: | 项 | 结果 | |---|---| | 路径 | `D:\DevApps\DockerDesktop\Docker Desktop.exe`(存在,15,142,832 字节) | | 启动 | `Start-Process -FilePath '…\Docker Desktop.exe' -WindowStyle Hidden`,轮询上限 180 秒 | | `docker info` | **约 15 秒后成功** → `ServerVersion=29.6.2` | | 原先跳过的 26 项 | **26 / 26 实际执行**,`Tests run: 26, Failures: 0, Errors: 0, Skipped: 0` | **最终全量(复验阶段)**: ``` mvn test → Tests run: 2743, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS mvn clean verify → Tests run: 2743, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS ``` `2743 = 2725(本报告阶段总数,含 26 项跳过)+ 18(独立复验新增测试)`; 其 **Skipped=0** 正是因为那 26 项已真实执行。**本阶段未改动任何代码即补足了这 26 项。** --- ## 17. OpenAPI ``` python ..\docs\verify_openapi.py → paths: 176 / operations: 214 → All checks passed! ``` `git diff -- docs/openapi-rc.json` → **空** ✔ --- ## 18. 精确修改文件 **生产代码变更总数:新增 1 个生产类、修改 2 个生产类,共 3 个生产文件。** **新增生产类(1)** ``` mydreamjob-server/src/main/java/com/mydreamjob/candidatescore/rules/CandidateScoringRuleSet.java ``` **修改生产类(2)** ``` candidatescore/CandidateScoringCalculator.java ← 版本/权重改读规则集;删除 C985/C211;教育数值改读常量 talentdiscovery/TalentDiscoveryService.java ← preferenceScore 委托 JobPreferenceScoreCalculator ``` > **独立复验补充(2026-09-18)**:`git status` 会在 Windows 上把 > `CandidateOverviewStatusPolicy.java` 与 `JobPreferenceScoreCalculator.java` 标为 ` M` > (变异实验重写过这两个文件、mtime 变化)。经 `git hash-object` 与 `git rev-parse HEAD:` > 比对,两者 blob **完全相同**,属 **Windows stat 缓存假阳性**,**不在本提交范围内**。 **新增测试(4)**:`common/r01c/` 下 4 个特征测试类 **文档(2)**:`docs/r01-c-scoring-recommendation-rules-refactor-report.md`(新增)、 `docs/backend-development-roadmap.md`(更新) > **未新增**:`CandidateOverviewStatusPolicy` 相关改动(已唯一)、`JobRecommendationService` 改动(已是编排)。 --- ## 19. Flyway / ErrorCode / DTO / 接口变化 | 项 | 结果 | |---|---| | Flyway / 迁移 | **未改动** ✔ | | `ErrorCode` | **未改动** ✔ | | DTO / 响应结构 | **未改动** ✔ | | 公网接口 / OpenAPI | **未改动**(176/214,`openapi-rc.json` 未变) ✔ | | Token / 权限 / requestId | **未改动** ✔ | | 依赖 / `pom.xml` | **未改动** ✔ | | 评分公式数值 / 排序 / 同分规则 / 小样本与互补风险抑制 / 申诉回滚 / 快照版本推进 / Fairness / 消息与 Offer 等 | **均未改动** ✔ | --- ## 20. Git 状态 | 项 | 结果 | |---|---| | 分支 / HEAD | `refactor/r01-structure-performance` / `9954600…`(未变) | | `git diff --cached --name-only` | **空** ✔ | | `git diff --check` | exit 0 ✔ | | 生产代码改动范围 | **恰为 2 个文件**,与 §18 一致(变异内容未残留)✔ | | 已暂存 / 已提交 / 已推送 | **均无** ✔ | | 历史未跟踪文件 | 未删除 ✔ | --- ## 21. 未暂存、未提交、未推送 **确认。** 未执行 `git add`;未创建提交;未推送;未执行 `reset`/`restore`/`checkout`/`clean`; 未进入 R01-E。 --- ## 22. 未进入 R01-E **确认。** 本轮未处理 N+1、会话未读数、`overviewStatus` 之外的其他查询收束、 数据库迁移或任何 R01-E 范围内的主题。 --- > **R01-C评分与推荐规则结构收束完成,待独立代码复验。**