From df7238b436cd186d00e259417d2cc6ea12b00d0e Mon Sep 17 00:00:00 2001 From: pwklppg <123456@.com> Date: Tue, 15 Sep 2026 10:36:59 +0800 Subject: [PATCH] docs: record W2 reviews and unlock W3 --- changes/n03-skill-cloud/.spec-superflow.yaml | 6 +- .../.superpowers/sdd/execution-plan.yaml | 13 +- .../.superpowers/sdd/progress.md | 9 + .../sdd/reviews/W2-T4-rereview.md | 130 +++++++++++++ .../.superpowers/sdd/reviews/W2-T4.md | 150 +++++++++++++++ .../sdd/reviews/W2-T5-rereview.md | 94 ++++++++++ .../.superpowers/sdd/reviews/W2-T5.md | 94 ++++++++++ .../sdd/reviews/W2-T6-rereview.md | 98 ++++++++++ .../.superpowers/sdd/reviews/W2-T6.md | 71 +++++++ .../sdd/reviews/W2-T7-rereview.md | 80 ++++++++ .../.superpowers/sdd/reviews/W2-T7.md | 72 +++++++ .../.superpowers/sdd/reviews/W2-wave.md | 175 ++++++++++++++++++ 12 files changed, 983 insertions(+), 9 deletions(-) create mode 100644 changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T4-rereview.md create mode 100644 changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T4.md create mode 100644 changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T5-rereview.md create mode 100644 changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T5.md create mode 100644 changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T6-rereview.md create mode 100644 changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T6.md create mode 100644 changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T7-rereview.md create mode 100644 changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T7.md create mode 100644 changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-wave.md diff --git a/changes/n03-skill-cloud/.spec-superflow.yaml b/changes/n03-skill-cloud/.spec-superflow.yaml index 81ab56a..c52748b 100644 --- a/changes/n03-skill-cloud/.spec-superflow.yaml +++ b/changes/n03-skill-cloud/.spec-superflow.yaml @@ -27,11 +27,13 @@ plan_revision: 1 dp_4_result: approved: SDD selected; W1 serial T1-T3, W2 serial T4-T7 depends W1, W3 serial T8-T10 depends W2 dp_4_confirmed: true dp_4_timestamp: 2026-09-14T08:00:29Z -batches_completed: 1 +batches_completed: 2 dp_5_result: adjudicated: approved one focused repair for exact per-skill source and full proxy assertions dp_5_timestamp: 2026-09-14T09:21:01Z -execution_paused: true +execution_paused: false paused_at: 2026-09-14T09:44:45Z +resumed_at: 2026-09-15T01:18:50Z +resume_count: 1 paused_reason: user requested immediate pause; W2 may only resume at T4 on manual request resume_from: T4 resume_mode: manual diff --git a/changes/n03-skill-cloud/.superpowers/sdd/execution-plan.yaml b/changes/n03-skill-cloud/.superpowers/sdd/execution-plan.yaml index a1964c0..cf332c5 100644 --- a/changes/n03-skill-cloud/.superpowers/sdd/execution-plan.yaml +++ b/changes/n03-skill-cloud/.superpowers/sdd/execution-plan.yaml @@ -2,10 +2,9 @@ plan_revision: 1 selected_mode: sdd contract_hash: 819762ae94f2cb3d11820904d013947c367e4bbb32fe8773a3510a253be9ca97 created_at: 2026-09-14T08:00:29Z -paused: true -paused_at: 2026-09-14T09:44:38Z -resume_from: T4 -current: W2 +paused: false +resumed_at: 2026-09-15T01:18:50Z +current: W3 waves: - id: W1 strategy: serial @@ -17,11 +16,11 @@ waves: strategy: serial tasks: [T4, T5, T6, T7] depends_on: [W1] - state: paused - receipt: null + state: passed + receipt: pass (W2-wave; 50 tests) - id: W3 strategy: serial tasks: [T8, T9, T10] depends_on: [W2] - state: blocked + state: eligible receipt: null \ No newline at end of file diff --git a/changes/n03-skill-cloud/.superpowers/sdd/progress.md b/changes/n03-skill-cloud/.superpowers/sdd/progress.md index df71813..678d8cf 100644 --- a/changes/n03-skill-cloud/.superpowers/sdd/progress.md +++ b/changes/n03-skill-cloud/.superpowers/sdd/progress.md @@ -12,3 +12,12 @@ - 2026-09-14T09:21:01Z DP-5 adjudication: continue with one focused test-falsifiability repair. - 2026-09-14T09:38:16Z Task T3: complete after adjudicated repair (final review PASS; 10 tests green; 221 exact mappings; W1 complete). - 2026-09-14T09:44:38Z User paused execution immediately. Active T4 agent was terminated before completion; partial uncommitted T4 files were removed. W1 remains passed. Resume manually from T4 only; do not start T5-T10 earlier. +- 2026-09-15T01:18:50Z Manual resume accepted. W1 remains passed; W2/T4 is eligible. T5-T10 remain blocked until predecessor reviews pass. +- 2026-09-15T01:29:44Z Task T4: failed attempt 1/3 - review W2-T4 FAIL; category join lacks same-occupation guard and test cannot falsify cross-position mismatch. +- 2026-09-15T01:37:17Z Task T4: complete (commits 0c77824, 52751e5; review W2-T4-rereview PASS; 16 tests). T5 eligible. +- 2026-09-15T01:48:56Z Task T5: failed attempt 1/3 - review W2-T5 FAIL; SkillCloudItem omitted sourceType/sourceCode. +- 2026-09-15T01:54:38Z Task T5: complete (commits e3eecfe, 1666162; review W2-T5-rereview PASS; 24 tests). T6 eligible. +- 2026-09-15T02:08:59Z Task T6: failed attempt 1/3 - review W2-T6 FAIL; tests do not isolate nameZh and alias matching branches. +- 2026-09-15T02:14:37Z Task T6: complete (commit 6eb30e7; review W2-T6-rereview PASS; 40 tests). T7 eligible. +- 2026-09-15T02:23:08Z Task T7: failed attempt 1/3 - review W2-T7 FAIL; missing-skill sequence test not falsifiable because names/IDs were already ascending. +- 2026-09-15T02:36:53Z W2 wave review PASS; 50 tests; W3/T8 eligible. Minor items carried to T8/T9. diff --git a/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T4-rereview.md b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T4-rereview.md new file mode 100644 index 0000000..02e18ff --- /dev/null +++ b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T4-rereview.md @@ -0,0 +1,130 @@ +# W2-T4 修复复审报告 + +## 状态 + +完成。本轮仅复审修复提交 `52751e5`,对照第一轮报告 `W2-T4.md`,未修改任何实现、测试、迁移或配置文件;仅新增本报告。 + +## Verdict + +**PASS** + +第一轮阻断项 `W2-T4-I1` 已关闭。两个投影查询均已保证技能分类的 `occupation_id` 与职位技能关系的 `occupation_id` 一致,新增测试真实构造了跨职位分类错配数据并验证查询排除该数据。规定回归集全部通过,未发现新的 Critical、Important 或阻断项。 + +## Finding 关闭状态 + +### W2-T4-I1:分类过滤查询未强制 category 与 occupation 属于同一职位 + +状态:**已关闭**。 + +关闭证据: + +- 整职位查询在分类关联后增加了 `c.occupationId = os.occupationId`: + `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:36-40` +- 职位加分类查询同样增加了该约束,并保留 `os.occupationId = :occupationId` 与 `os.categoryId = :categoryId`: + `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:63-69` +- 两个查询均继续通过 `os.skillId = s.id` 和 `os.categoryId = c.id` 连接技能及分类,未改变投影字段或原过滤条件。 + +## 独立测试结果 + +环境: + +- `JAVA_HOME=D:\java\jdk` +- Java:OpenJDK 17.0.16 +- Maven:`D:\maven\apache-maven-3.9.7\bin\mvn.cmd` +- 工作目录:`D:\1\zhi\zhilai\mydreamjob-server` + +命令: + +```powershell +$env:JAVA_HOME='D:\java\jdk' +& 'D:\maven\apache-maven-3.9.7\bin\mvn.cmd' '-Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest' test +``` + +结果: + +```text +SkillCloudSeedDataIntegrationTest: Tests run: 8, Failures: 0, Errors: 0, Skipped: 0 +SkillTaxonomyMigrationTest: Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 +SkillTaxonomyRepositoryTest: Tests run: 6, Failures: 0, Errors: 0, Skipped: 0 +Total: Tests run: 16, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +Total time: 40.769 s +``` + +## 必须检查项逐项结论 + +### 1. 两个投影查询均约束 category 所属 occupation,且保留过滤:通过 + +两个查询都在连接 `os.categoryId = c.id` 后增加: + +```sql +AND c.occupationId = os.occupationId +``` + +整职位查询继续只按目标 `occupationId` 过滤;职位加分类查询继续同时按目标 `occupationId` 和 `categoryId` 过滤。因此: + +- 不会再通过职位 A 的关系读取仅属于职位 B 的分类; +- 分类参数仍必须与关系中的分类 ID 完全相等; +- 职位参数语义未改变。 + +位置: + +- `OccupationSkillRepository.java:36-43` +- `OccupationSkillRepository.java:63-72` + +### 2. 新增测试真实插入跨职位错配关系,且可证伪修复:通过 + +新增测试位于 `D:\1\zhi\zhilai\mydreamjob-server\src\test\java\com\mydreamjob\skill\SkillTaxonomyRepositoryTest.java:123-152`。 + +测试通过 `JdbcTemplate` 向真实 `occupation_skills` 表插入: + +- `occupation_id = 1` +- `category_id = 201` +- `skill_id = 1` +- `source_version_id = 2` + +其中分类 `201` 在 V36 中明确属于职位 `2`,见 `V36__skill_cloud_seed.sql:67`;技能 `1` 存在,见 `V36__skill_cloud_seed.sql:129`。插入使用合法外键值和完整来源字段,因此构造的是数据库允许保存、但职位与分类不属于同一节点的逻辑错配关系。 + +该测试没有断言 SQL 文本,而是调用真实 Repository 方法并检查返回结果。修复前的查询缺少同职位约束,插入关系会满足 `os.skillId = s.id`、`os.categoryId = c.id` 和 `os.occupationId = 1`,因此: + +- 整职位查询会包含技能 `1`,在第 `142` 行 `doesNotContain(1L)` 失败; +- `occupation=1, category=201` 查询会包含该关系,在第 `144-146` 行失败。 + +修复后该关系被 `c.occupationId = os.occupationId` 排除,测试通过,可证伪性成立。 + +测试方法带 `@Transactional`,见 `SkillTaxonomyRepositoryTest.java:123-124`,插入数据在测试结束后回滚,不污染其他测试或 V36 计数。 + +### 3. 三项断言均正确覆盖:通过 + +`SkillTaxonomyRepositoryTest.java:136-151` 分别验证: + +- 整职位查询仍包含正常技能 `3`,且不包含错配技能 `1`;当前断言在 `139-142` 行。 +- 错误分类组合 `occupation=1, category=201` 返回空列表;当前断言在 `144-146` 行。 +- 正确组合 `occupation=2, category=201` 仍返回技能 `1`,证明修复没有误伤合法数据;当前断言在 `148-151` 行。 + +三项断言覆盖了第一轮要求的安全边界和正常路径。 + +### 4. 正常查询、空 Importance、来源字段和计数:通过 + +- 原整职位和分类过滤测试继续验证职位 `1` 返回 15 项、分类 `101` 返回精确技能集合,以及错误职位分类返回空:`SkillTaxonomyRepositoryTest.java:95-120`。 +- O*NET 来源字段、`importance`、`importanceSourceCode`、`importanceElementId`、代理标记和排序字段的原断言仍通过:`SkillTaxonomyRepositoryTest.java:154-188`。 +- Figma 的空 Importance 场景仍通过:`SkillTaxonomyRepositoryTest.java:176-188`。 +- V36 计数仍为来源版本 2、行业 3、职位 10、分类 68、技能 72、别名 72、职位技能关系 139:`SkillTaxonomyRepositoryTest.java:72-81`。 +- 指定回归集 16 项全部通过,未观察到由新增约束造成的正常查询、投影字段或种子数据回归。 + +### 5. 提交范围与 T5 边界:通过 + +`git diff --name-only 0c77824..52751e5` 仅列出: + +- `mydreamjob-server/src/main/java/com/mydreamjob/skill/repository/OccupationSkillRepository.java` +- `mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillTaxonomyRepositoryTest.java` + +未修改 V35/V36、生产配置、安全配置或其他模块,也未实现任何 T5 排名、匹配或学习路径代码。`git diff --check 0c77824..52751e5` 通过。 + +## 剩余阻断项 + +无。 + +## 非阻断备注 + +V35 仍允许数据库层面写入 `occupation_id` 与 `category_id` 逻辑不一致的关系,因为当前外键是单列外键;本次修复保证 Repository 查询不会返回此类错配数据。是否需要增加数据库复合完整性约束属于独立的数据模型变更,不影响 T4 通过。 diff --git a/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T4.md b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T4.md new file mode 100644 index 0000000..e2ace83 --- /dev/null +++ b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T4.md @@ -0,0 +1,150 @@ +# W2-T4 独立评审报告 + +## 状态 + +完成。本轮只评审提交 `0c77824`、修订后的契约与规划产物、V35/V36,并独立执行指定测试;未修改实现、测试、迁移或配置文件,仅写入本报告。 + +## Verdict + +**FAIL** + +未发现 Critical。发现 1 项 Important:技能投影查询没有保证 `occupation_skills.category_id` 对应的分类属于同一个 `occupation_id`,在数据库出现逻辑错配关系时会返回跨职位数据。该问题会阻断 T4 通过,修复并复审后可进入 T5。 + +## 独立测试输出 + +环境: + +- `JAVA_HOME=D:\java\jdk` +- Java:OpenJDK 17.0.16 +- Maven:`D:\maven\apache-maven-3.9.7\bin\mvn.cmd` +- 工作目录:`D:\1\zhi\zhilai\mydreamjob-server` + +命令: + +```powershell +$env:JAVA_HOME='D:\java\jdk' +& 'D:\maven\apache-maven-3.9.7\bin\mvn.cmd' '-Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest' test +``` + +结果: + +```text +SkillCloudSeedDataIntegrationTest: Tests run: 8, Failures: 0, Errors: 0, Skipped: 0 +SkillTaxonomyMigrationTest: Tests run: 2, Failures: 0, Errors: 0, Skipped: 0 +SkillTaxonomyRepositoryTest: Tests run: 5, Failures: 0, Errors: 0, Skipped: 0 +Total: Tests run: 15, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +Total time: 44.358 s +``` + +## 必须检查项逐项结论 + +### 1. 实体与 V35 映射:通过 + +7 个实体均映射到 V35 对应表,主键为 `Long` + `GenerationType.IDENTITY`。已逐表核对表名、列名、长度、可空性和类型: + +- `SkillSourceVersion` 与 `skill_source_versions`:字段、长度和可空性一致。 +- `SkillIndustry` 与 `skill_industries`:字段、长度和可空性一致。 +- `SkillOccupation` 与 `skill_occupations`:字段、长度和可空性一致。 +- `SkillCategory` 与 `skill_categories`:字段、长度和可空性一致。 +- `Skill` 与 `skills`:字段、长度和可空性一致。 +- `SkillAlias` 与 `skill_aliases`:字段、长度和可空性一致;该表无 `updated_at`,实体也未映射该列。 +- `OccupationSkill` 与 `occupation_skills`:`DECIMAL(4,2)`、来源字段长度、`TINYINT(1)` 代理标记及可空性匹配;`importanceIsProxy` 使用 `@JdbcTypeCode(SqlTypes.TINYINT)`。 + +本次测试在 `ddl-auto=validate` 下执行通过,证明 V35 七张表实际参与了 Hibernate 校验。 + +### 2. Schema Filter 与生产配置:通过 + +`SkillTaxonomyRepositoryTest` 的 validate filter 明确包含全部七个 V35 表: + +`D:\1\zhi\zhilai\mydreamjob-server\src\test\java\com\mydreamjob\skill\SkillTaxonomyRepositoryTest.java:182-231` + +因此该 filter 隔离的是仓库历史中与本任务无关的表,不会跳过 V35 实体的列类型或可空性校验。测试通过 `@TestPropertySource` 覆盖测试数据库和 `ddl-auto`,提交未修改生产 `application.yml`、`SecurityConfig` 或其他配置。 + +### 3. 行业、职位、分类关联校验:通过 + +以下 Repository 方法正确表达了单节点关系约束: + +- `SkillOccupationRepository.existsByIdAndIndustryId` / `findByIdAndIndustryId` +- `SkillCategoryRepository.existsByIdAndOccupationId` / `findByIdAndOccupationId` + +测试覆盖了正确组合、跨行业职位、跨职位分类和不存在的 ID: + +`D:\1\zhi\zhilai\mydreamjob-server\src\test\java\com\mydreamjob\skill\SkillTaxonomyRepositoryTest.java:78-88` + +### 4. occupation + 可选 category 查询的同职位约束:不通过 + +两个投影查询都只连接 `os.category_id = c.id`,没有断言 `c.occupation_id` 与 `os.occupation_id` 相同: + +`D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:36-40` + +`D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:62-66` + +V35 对 `occupation_skills.category_id` 只有单列外键,不阻止一条关系的 `occupation_id` 指向职位 A、`category_id` 指向职位 B 的分类: + +`D:\1\zhi\zhilai\mydreamjob-server\src\main\resources\db\migration\V35__skill_cloud_taxonomy.sql:114-117` + +这违反了契约要求“分类必须属于职位”: + +`D:\1\zhi\zhilai\changes\n03-skill-cloud\execution-contract.md:59` + +也违反了设计中的“category_id 必须属于 occupation_id”: + +`D:\1\zhi\zhilai\changes\n03-skill-cloud\design.md:119` + +现有测试 `findSkillProjectionsByOccupationIdAndCategoryId(2L, 101L)` 只证明当前种子数据中不存在 `职位 2 + 分类 101` 关系,不能发现上述缺列条件;即使插入一条 `occupation_id=2, category_id=101` 的逻辑错配关系,该查询仍会返回它: + +`D:\1\zhi\zhilai\mydreamjob-server\src\test\java\com\mydreamjob\skill\SkillTaxonomyRepositoryTest.java:113-115` + +修复方向明确:两个查询均增加分类所属职位约束,例如 `AND c.occupationId = os.occupationId`;同时增加可证伪测试,用跨职位分类关系验证查询不会返回数据。 + +### 5. 投影 14 个字段:通过 + +`SkillCloudSkillProjection` 包含 14 个字段,覆盖技能身份、名称、来源、分类、`importance`、`importanceSourceCode`、`importanceElementId`、`importanceIsProxy`、技能排序值和分类排序值。构造表达式与字段顺序一致,`importance` 使用 `BigDecimal`,代理标记使用 `Boolean`。 + +`D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\dto\SkillCloudSkillProjection.java:5-20` + +### 6. 未提前实现 T5–T8:通过 + +提交只新增 7 个实体、1 个投影、7 个 Repository 和专项测试;未新增排名服务、匹配服务、学习路径服务、Controller、安全规则或 OpenAPI 变更。Repository 查询明确未实现最终排序,符合 T5 边界。 + +### 7. 测试数据覆盖与可证伪性:除跨职位查询外通过 + +测试使用了 V36 种子数据,覆盖计数、关联校验、整职位查询、可选分类过滤、O*NET 来源、空 Importance、代理标记、别名和按 ID 查询。查找均为精确断言,不是仅计数。 + +但第 4 项的跨职位分类错配缺少可证伪用例;即使 Repository 查询遗漏同职位条件,当前 15 项测试仍全部通过。 + +### 8. 提交范围与历史迁移:通过 + +提交只包含允许的 16 个文件。独立哈希核对确认 V35/V36 在 `0c77824` 中未发生变化,提交未修改其他模块、已有迁移、生产配置或安全配置。 + +## Findings + +### Critical + +无。 + +### Important + +#### W2-T4-I1:分类过滤查询未强制 category 与 occupation 属于同一职位 + +证据: + +- `OccupationSkillRepository` 两个查询均只有 `os.categoryId = c.id`,缺少 `c.occupationId = os.occupationId`: + - `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:36-40` + - `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:62-66` +- V35 外键不提供复合 `(occupation_id, category_id)` 完整性: + - `D:\1\zhi\zhilai\mydreamjob-server\src\main\resources\db\migration\V35__skill_cloud_taxonomy.sql:114-117` +- 契约明确要求分类必须属于职位: + - `D:\1\zhi\zhilai\changes\n03-skill-cloud\execution-contract.md:59` + - `D:\1\zhi\zhilai\changes\n03-skill-cloud\design.md:119` + +影响:一旦数据写入出现逻辑错配,整职位查询会返回其他职位的分类技能;带分类查询也可能在对应关系存在时返回跨职位数据,破坏词云筛选边界,并影响后续匹配和学习路径。当前种子数据恰好一致,因此测试未暴露问题。 + +### Minor + +无额外阻断项。 + +## 未决问题 + +无。修复语义明确:查询必须同时绑定 category 所属 occupation;修复后应增加一条跨职位分类关系的失败用例并重新评审。 diff --git a/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T5-rereview.md b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T5-rereview.md new file mode 100644 index 0000000..82f570c --- /dev/null +++ b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T5-rereview.md @@ -0,0 +1,94 @@ +# W2-T5 聚焦修复独立复审报告 + +- **评审对象**:T5 I-1 聚焦修复工作区差异(基于 `e3eecfe`) +- **前次评审**:`changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T5.md`,结论 `FAIL` +- **复审范围**:仅检查 `SkillCloudItem.java`、`SkillCloudQueryService.java`、`SkillCloudQueryServiceTest.java` 及其对既有行为的影响 +- **结论**:**PASS** +- **剩余阻断项**:无 + +## Finding 关闭状态 + +### [I-1] 响应项遗漏技能来源标识:已关闭 + +修复已同时补齐 DTO 字段和 Service 映射: + +- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\dto\SkillCloudItem.java:11-12` + - 新增 `sourceType` + - 新增 `sourceCode` +- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\SkillCloudQueryService.java:102-117` + - `toItem(...)` 在 `nameEn` 后依次映射 `projection.sourceType()` 和 `projection.sourceCode()`(第 109-110 行) + - 热度来源仍独立映射为 `heatSourceCode`、`heatElementId`、`heatProxy`(第 114-116 行) + +字段语义未混淆:技能自身来源是 `sourceType/sourceCode`,O*NET Importance 来源是 `heatSourceCode/heatElementId/heatProxy`。 + +### 双来源测试与可证伪性:已关闭 + +`D:\1\zhi\zhilai\mydreamjob-server\src\test\java\com\mydreamjob\skill\SkillCloudQueryServiceTest.java:198-236` 同时锁定两类来源: + +- O*NET 技术来源:第 219-220 行精确断言 + - `sourceType = "O*NET"` + - `sourceCode = "15-1254.00|React|2.E.7.h"` +- ESCO 技能来源:第 232-234 行精确断言 + - `sourceType = "ESCO"` + - `sourceCode = "http://data.europa.eu/esco/skill/3cd569a2-4f88-4c1e-9995-8dce8c5e51a7"` +- 来源隔离:第 235 行额外断言 JavaScript 的 `heatSourceCode = "15-1254.00"`,与 ESCO `sourceCode` 明显不同。 + +因此: + +- 若 `sourceType` 或 `sourceCode` 映射被遗漏并传为 `null`,上述精确断言会失败。 +- 若二者被交换,O*NET 与 ESCO 的精确值断言会失败。 +- 若技能来源与热度来源混用,React 的完整 O*NET 三元组断言及 JavaScript 的 URI/SOC 对照断言会失败。 + +## 原有行为回归 + +独立执行指定环境与命令: + +```powershell +$env:JAVA_HOME='D:\java\jdk' +& 'D:\maven\apache-maven-3.9.7\bin\mvn.cmd' '-Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest,SkillCloudQueryServiceTest' test +``` + +结果: + +```text +Tests run: 24, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +``` + +Surefire 分类计数: + +- `SkillTaxonomyMigrationTest`:2 +- `SkillCloudSeedDataIntegrationTest`:8 +- `SkillTaxonomyRepositoryTest`:6 +- `SkillCloudQueryServiceTest`:8 + +已核验原有行为: + +- 全量查询、连续排名:`SkillCloudQueryServiceTest.java:62-81` +- 热度降序、同热度名称排序、null heat 末位:`SkillCloudQueryServiceTest.java:84-107` +- 同值排序的 `skillId` 稳定性:`SkillCloudQueryServiceTest.java:109-145` +- 分类过滤及缺省分类:`SkillCloudQueryServiceTest.java:166-179` +- 合法空分类返回空列表:`SkillCloudQueryServiceTest.java:181-196` +- 必填/正整数/未知节点/跨行业职位/跨职位分类校验:`SkillCloudQueryServiceTest.java:238-264` +- provenance 字段映射:`SkillCloudQueryServiceTest.java:198-236` + +本次修复没有改变排序、过滤、校验或热度来源映射逻辑,仅扩展响应字段与测试断言。 + +## 变更范围 + +自 `e3eecfe` 起,T5 实现差异仅包含: + +- `SkillCloudQueryService.java` +- `SkillCloudItem.java` +- `SkillCloudQueryServiceTest.java` + +共 `20 insertions, 3 deletions`。未修改 V35/V36、Repository、安全配置或应用配置;未发现 T6/T7/T8 的 Controller、匹配度 Service、学习路径 Service 或权限配置实现。 + +## 非阻断备注 + +- **M-1 仍为 Minor**:设计文档中的 `heatSource` 嵌套形态与当前三个扁平热度来源字段尚未统一,按前次评审要求留待 T8 明确 OpenAPI 和接口文档。 +- **M-2 仍为 Minor**:Service 层未单独断言无 heat 技能的 `sourceType/sourceCode`;Repository 层已在该场景断言 Figma 保留来源(`SkillTaxonomyRepositoryTest.java:183-188`),且 Service 映射无条件执行,不构成阻断。 + +## 最终裁决 + +`PASS`。I-1 已关闭,指定联合回归全部通过,未发现新的 Critical 或 Important 问题;T5 可进入后续任务门禁。 \ No newline at end of file diff --git a/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T5.md b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T5.md new file mode 100644 index 0000000..c33d614 --- /dev/null +++ b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T5.md @@ -0,0 +1,94 @@ +# W2-T5 独立评审报告 + +- **评审对象**:`e3eecfe feat: add skill cloud query service` +- **评审基线**:`52751e5`(T4 复审通过提交) +- **结论**:**FAIL** +- **评审范围**:仅检查提交 `e3eecfe` 的 5 个文件;未修改实现文件。 + +## 独立测试结果 + +环境:`JAVA_HOME=D:\java\jdk`,Maven `D:\maven\apache-maven-3.9.7\bin\mvn.cmd`。 + +执行: + +```powershell +mvn -Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest,SkillCloudQueryServiceTest test +``` + +结果: + +```text +Tests run: 24, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +``` + +测试数量拆分为:迁移 2、种子数据 8、Repository 6、词云 Service 8。 + +## Important + +### [I-1] 响应项遗漏技能自身的来源标识,违反 T5 的“来源字段”和可追溯要求 + +`SkillCloudSkillProjection` 已查询并携带技能来源字段: + +- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\dto\SkillCloudSkillProjection.java:10-11` + - `sourceType` + - `sourceCode` +- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:25-26` + - 查询投影包含 `s.sourceType` 和 `s.sourceCode` +- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\repository\OccupationSkillRepository.java:52-53` + - 分类查询同样包含这两个来源字段 + +但 Service 转换成响应 DTO 时丢弃了这两个字段: + +- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\SkillCloudQueryService.java:102-115` + - `toItem(...)` 没有传入 `projection.sourceType()` 或 `projection.sourceCode()` +- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\dto\SkillCloudItem.java:5-17` + - Item 只有 `heatSourceCode`、`heatElementId`、`heatProxy`,没有技能来源类型或技能来源编码 + +这会使返回结果无法在接口层区分某个技能来自 ESCO 还是 O*NET `software_skills`,只剩通过数据库内部 ID 才能反查来源。T5 明确要求返回“来源字段”: + +- `D:\1\zhi\zhilai\changes\n03-skill-cloud\tasks.md:21` +- `D:\1\zhi\zhilai\changes\n03-skill-cloud\execution-contract.md:41` +- `D:\1\zhi\zhilai\changes\n03-skill-cloud\specs\skill-cloud\spec.md:86-93` + +当前测试也没有覆盖该缺口:`SkillCloudQueryServiceTest` 只断言了 React 的 `heatSourceCode`、`heatElementId` 和 `heatProxy`(`SkillCloudQueryServiceTest.java:198-223`),没有断言 Item 中应携带 `sourceType`/`sourceCode`。因此该遗漏是可复现但不可被现有测试发现的。 + +**修复要求**:在 `SkillCloudItem` 中补齐技能来源字段,在 `toItem` 中映射,并增加至少一个 ESCO 技能和一个 O*NET `software_skills` 技能的精确断言;同时确认无 heat 的技能仍保留其技能来源。 + +## Minor + +### [M-1] `heatSource` 的响应形态与设计文档不一致,需在 T8 前统一 + +设计文档将词云项描述为包含 `heatSource`: + +- `D:\1\zhi\zhilai\changes\n03-skill-cloud\design.md:131` + +当前实现使用三个扁平字段表达同一语义: + +- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\dto\SkillCloudItem.java:13-16` + - `heatSourceCode` + - `heatElementId` + - `heatProxy` + +这不是当前 T5 的运行时错误,但 T8 必须明确 OpenAPI 和接口文档采用扁平字段还是嵌套 `heatSource` 对象,避免契约漂移。 + +### [M-2] T5 服务测试对 null heat 的来源字段覆盖不足 + +`SkillCloudQueryServiceTest` 对 React 的非空 heat 来源做了精确断言,但没有对无 heat 技能(如当前种子数据中的 Figma)断言 `heatSourceCode`、`heatElementId` 和 `heatProxy` 的最终 DTO 值。T4 投影测试覆盖了该场景,因此暂不阻断;修复 I-1 时应顺带补齐服务层断言。 + +## 已通过检查 + +- 行业存在性、职位存在性、行业—职位归属校验均使用 `BusinessException(ErrorCode.BAD_REQUEST)`。 +- 非法 `industryId`、`occupationId`、`categoryId`(null、0、负数)均返回 400 语义;未知节点和跨行业/跨职位节点同样返回 400。 +- `categoryId` 缺省时调用整职位查询;传入时调用职位+分类查询,并保留 Repository 的职位—分类归属约束。 +- 排序实现为 `heat DESC`、null heat 最后、`nameZh ASC`、`skillId ASC`;当前种子数据测试能捕获名称比较器和 skillId 比较器写错,也覆盖了 null heat 末位和连续 rank。 +- 空分类返回成功、`total=0`、空 items。 +- `SkillCloudResponse` 对列表执行 `List.copyOf`,响应列表不可变。 +- 提交范围仅包含 T5 的 Service、3 个 DTO 和测试文件,没有实现 T6/T7/T8,也没有修改 V35/V36、安全配置或既有接口。 +- 未发现事务、空指针、可变集合、随机性或明显性能问题;当前静态词库规模下三次校验查询加一次投影查询是合理的。 + +## 未决问题 + +1. 技能来源字段的最终 JSON 名称应为 `sourceType`/`sourceCode`,还是采用带 `skillSource` 前缀的名称;需在修复 I-1 和 T8 OpenAPI 时统一。 +2. `heatSource` 是否需要改成嵌套对象;若保持扁平字段,应同步修订设计文档和后续接口文档。 +3. I-1 修复后必须重新执行本报告使用的 24 项联合回归,并重新评审;T5 评审通过前不得进入 T6。 diff --git a/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T6-rereview.md b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T6-rereview.md new file mode 100644 index 0000000..85374e8 --- /dev/null +++ b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T6-rereview.md @@ -0,0 +1,98 @@ +# W2-T6 测试隔离修复独立复审报告 + +## Verdict + +**PASS** + +T6 原 Review 中唯一 Important finding 已关闭。新增的三个隔离测试均通过服务返回结果进行断言,能够分别证伪 `nameZh` 分支、alias 匹配链路和 alias 归属隔离的相关回归;类级事务确保测试数据回滚。未发现新的阻断项,T6 可进入下一任务。 + +## Finding 关闭状态 + +### T6-I1:`nameZh` 与 alias 匹配遗漏无法证伪 + +**状态:已关闭。** + +1. **nameZh-only 隔离成立** + - 测试:`matchesOnlyNameZhForV36JavaWhenOtherIdentityFieldsDoNotMatch` + - 位置:`D:\1\zhi\zhilai\mydreamjob-server\src\test\java\com\mydreamjob\skill\SkillMatchServiceTest.java:237-252` + - 第 239 行先将 V36 Java 的 `code` 改为 `JAVA_NAME_ZH_ISOLATED`。 + - Java 的 V36 `nameZh` 为 `Java`,`nameEn` 为 `Java (computer programming)`,alias 为 `Java (computer programming)`;学生输入仅为 `Java`。 + - 因此学生输入只能匹配 `nameZh`。删除生产代码的 `nameZh` 分支后,Java 必然进入 `missingSkills`,方法第 246-251 行会失败。 + +2. **alias-only 隔离成立** + - 测试:`matchesOnlyInsertedAliasForV36JavaWhenAliasIsTheOnlyIdentityMatch` + - 位置:`SkillMatchServiceTest.java:254-272` + - 第 256-259 行插入 Java 专属 alias `ONLY.ALIAS-JVM-XYZ`,与目标技能的 `nameZh`、`nameEn`、`skillCode` 均不相同。 + - 学生输入只有该 alias,并直接断言服务返回的 `matchedSkills` 包含 Java、`missingSkills` 包含 Python。 + - 删除 alias 查询或 alias 匹配分支后,Java 不会命中,测试必然失败。 + +3. **alias 归属隔离成立** + - 测试:`doesNotApplyAliasOwnedByAnotherTopSkillToV36Java` + - 位置:`SkillMatchServiceTest.java:274-292` + - 第 276-279 行将唯一 alias 关联到技能 2(Python),学生只输入该 alias。 + - 断言要求技能 2 命中、技能 1 缺失;若 alias 被错误共享给其他 TOP 技能,技能 1 也会命中,分数的断言会失败。 + +### 事务与回滚 + +**状态:满足要求。** + +- `SkillMatchServiceTest` 在类级使用 `@Transactional`:`SkillMatchServiceTest.java:51-52`。 +- 未发现 `@Commit` 或 `@Rollback(false)`。 +- 三个新增测试的 JDBC 插入/更新和 `service.match(...)` 调用均在测试事务内执行,测试结束后由 Spring Test 默认回滚,不会污染共享 H2 或 V36 数据。 + +### 服务结果断言 + +**状态:满足要求。** + +- 三个新增测试均直接对 `SkillMatchResponse` 的 `matchScore()`、`matchedSkills()` 和 `missingSkills()` 做断言,不是仅验证 helper。 +- alias-only 测试还会在第 265-271 行同时校验命中与缺失列表,能够发现“匹配分数正确但技能集合错误”的实现偏差。 + +## 独立测试结果 + +工作目录:`D:\1\zhi\zhilai\mydreamjob-server` + +环境: + +- `JAVA_HOME=D:\java\jdk` +- Maven:`D:\maven\apache-maven-3.9.7\bin\mvn.cmd` + +执行命令: + +```powershell +$env:JAVA_HOME='D:\java\jdk' +& 'D:\maven\apache-maven-3.9.7\bin\mvn.cmd' '-Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest,SkillCloudQueryServiceTest,SkillMatchServiceTest' test +``` + +结果: + +```text +Tests run: 40, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +Total time: 47.828 s +``` + +测试组成: + +| 测试类 | 数量 | Failures | Errors | +|---|---:|---:|---:| +| `SkillTaxonomyMigrationTest` | 2 | 0 | 0 | +| `SkillCloudSeedDataIntegrationTest` | 8 | 0 | 0 | +| `SkillTaxonomyRepositoryTest` | 6 | 0 | 0 | +| `SkillCloudQueryServiceTest` | 8 | 0 | 0 | +| `SkillMatchServiceTest` | 16 | 0 | 0 | + +## 回归与范围核验 + +- T6 原有 13 项行为测试全部保留,新增 3 项隔离测试,专项测试总数为 16。 +- `nameEn`、`skillCode`、去重、TOP10、分母、非法 JSON、空数据、排序和跨职位隔离等既有测试均通过。 +- `git diff --name-only -- mydreamjob-server/src/main/java/com/mydreamjob/skill/SkillMatchService.java mydreamjob-server/src/main/java/com/mydreamjob/skill/SkillNameNormalizer.java` 无输出,生产实现相对当前审阅基线未被本次测试修复修改。 +- 源码搜索未发现 `LearningPath`、`SkillCloudController`、`SkillMatchController` 或相关接口实现;T7/T8 未实现。 +- `git diff --check` 未报告代码差异错误;输出中的无关 `.trae` 文档警告不属于 T6 范围。 + +## 剩余阻断项 + +无。 + +## 结论 + +T6-I1 已关闭,T6 测试隔离修复满足可证伪性、事务隔离和服务结果断言要求。允许 T6 进入后续流程,但在流程门禁正式记录 `PASS` 前不得启动 T7。 diff --git a/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T6.md b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T6.md new file mode 100644 index 0000000..e9d4c65 --- /dev/null +++ b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T6.md @@ -0,0 +1,71 @@ +# W2-T6 独立评审报告 + +## Verdict + +**FAIL** + +T6 生产实现的主流程与当前契约基本一致,但测试未满足本次评审明确要求的可证伪性,尤其无法发现 `nameZh` 匹配或别名匹配被删除的回归。必须先补齐隔离型测试并通过复审,才能进入 T7。 + +## 独立测试结果 + +环境: + +- `JAVA_HOME=D:\java\jdk` +- Maven:`D:\maven\apache-maven-3.9.7\bin\mvn.cmd` +- 工作目录:`D:\1\zhi\zhilai\mydreamjob-server` + +命令: + +```powershell +$env:JAVA_HOME='D:\java\jdk' +& 'D:\maven\apache-maven-3.9.7\bin\mvn.cmd' '-Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest,SkillCloudQueryServiceTest,SkillMatchServiceTest' test +``` + +结果: + +```text +Tests run: 37, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +Total time: 58.642 s +``` + +测试组成:迁移 2、种子数据 8、Repository 6、词云查询 8、个人匹配 13。 + +## 阻断项 + +### Important:无法证伪 `nameZh` 与 alias 匹配遗漏 + +文件: + +- `D:\1\zhi\zhilai\mydreamjob-server\src\test\java\com\mydreamjob\skill\SkillMatchServiceTest.java:212-235` +- `D:\1\zhi\zhilai\mydreamjob-server\src\main\java\com\mydreamjob\skill\SkillMatchService.java:137-142` + +问题: + +- 测试方法 `matchesNameZhNameEnSkillCodeAndAliasAndDeduplicatesStudentSkills` 试图一次覆盖 `nameZh`、`nameEn`、`skillCode` 和别名,但没有隔离各候选来源。 +- 技能 1 的 `nameZh` 为“数据库”,同时注册别名 `JVM Language`。学生技能数组同时包含“数据库”和 `jvm-language`。 +- 如果删除 `SkillMatchService.matches(...)` 中的 `nameZh` 判断,技能 1 仍可通过别名命中,测试继续通过。 +- 如果删除 alias 查询、alias 分支或整个别名匹配链路,技能 1 仍可通过 `nameZh` 命中,测试同样继续通过。 +- 因此现有测试不能证明 REQ-SM-3 和 TO-5 中“标准名称 + 别名”匹配能力存在;这是本次评审明确要求验证的回归保护,不能仅依赖实现代码的静态正确性。 + +要求的修复方向: + +- 增加独立技能/独立场景,确保某个标准技能只能通过 `nameZh` 命中,另一个只能通过 alias 命中。 +- 保留现有 `nameEn` 和 `skillCode` 隔离断言。 +- 建议再断言 alias 必须归属于目标 `skillId`,避免别名被错误应用于其他标准技能时测试仍通过。 + +## 已核验且未发现阻断的行为 + +- 匹配集合直接对 `SkillCloudQueryService.query(...)` 的结果取 `subList(0, min(10, size))`,服务内未重新排序。 +- 无 profile、空白 JSON、空数组和语法损坏 JSON 均按无技能处理;损坏 JSON 测试验证不抛 500 且日志不含原始 JSON。 +- 归一化覆盖 NFKC、trim、`Locale.ROOT` 小写、Unicode 连字符、空白折叠、句点移除,并保留 `+`、`#`。 +- `nameEn` 和 `skillCode` 在现有测试中有可区分断言;归一化后的学生技能使用 `Set` 去重。 +- 分数使用 `Math.round(matched * 100.0 / actualTopSize)`,`actualTopSize=0` 返回 0,`1/3=33`,少于十条和恰好十条均有断言。 +- 命中/缺失列表按 TOP 顺序构建,并无额外排序;无匹配场景的并集顺序与词云 TOP10 对齐。 +- 响应 DTO 只包含技能投影,不包含原始 `skills_json`、手机号、邮箱或简历内容。 +- T6 为只读事务,无持久化;未实现 T7/T8。 + +## 未决问题 + +- 无设计或接口语义待裁决。 +- 待修复 T6 测试可证伪性后,必须重新执行同一回归命令并进行独立复审;复审 `PASS` 前不得启动 T7。 diff --git a/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T7-rereview.md b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T7-rereview.md new file mode 100644 index 0000000..5264be0 --- /dev/null +++ b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T7-rereview.md @@ -0,0 +1,80 @@ +# W2-T7 顺序测试修复复审报告 + +## Verdict + +**PASS** + +- 复审对象:`mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillLearningPathServiceTest.java` +- 对照报告:`changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T7.md` +- 生产服务基线:`fca1160 feat: add skill learning path service` +- 结论:T7-I1 已关闭;当前顺序测试可证伪,原有 T7 行为测试未回归,未发现 T8 实现。 + +## Finding 关闭状态 + +### T7-I1:测试无法阻止学习路径按名称重排 + +**已关闭。** + +1. Mockito 场景的输入名称和 ID 均不等于自然升序: + - `SkillLearningPathServiceTest.java:135-139` + - 名称输入为 `Skill Z -> Skill A -> Skill M`,自然升序为 `Skill A -> Skill M -> Skill Z`。 + - ID 输入为 `30 -> 10 -> 20`,自然升序为 `10 -> 20 -> 30`。 + - `SkillLearningPathServiceTest.java:155-156` 显式断言输入名称、ID 都不等于自然排序结果。 + - `SkillLearningPathServiceTest.java:157-165` 读取服务返回对象,并严格断言返回 ID 为 `30, 10, 20`、名称为 `Skill Z, Skill A, Skill M`。 + - 因此,生产实现若按名称或 ID 重排,现有测试必然失败。 + +2. 断言读取的是服务返回结果,而不是只验证 Mockito 输入: + - `SkillLearningPathServiceTest.java:157-165` 使用 `response.items()` 的 `skillId` 和 `skillName` 做精确顺序断言。 + - `SkillLearningPathServiceTest.java:160-162` 同时断言优先级严格为 `1, 2, 3`。 + - 满足“返回顺序、priority 和技能 ID”均被验证的要求。 + +3. 新增 V36 真实数据场景覆盖不同热度分组和名称顺序: + - `SkillLearningPathServiceTest.java:89-121` + - 使用教育行业 `3`、教学设计职位 `9` 调用真实 `SkillMatchService` 和 `SkillLearningPathService`。 + - `SkillLearningPathServiceTest.java:102-110` 断言缺失技能共 10 项、存在多个不同热度值,且名称和 ID 顺序均不等于自然升序。 + - `SkillLearningPathServiceTest.java:112-120` 将服务返回的 `skillId`、`skillName` 与 `matchResponse.missingSkills()` 的顺序做严格比较,并断言优先级为 `1..10`。 + - 因此,真实数据场景不是仅验证输入或数量,而是验证了学习路径严格复用匹配结果的缺失技能顺序。 + +4. 原有 T7 行为测试未回归: + - 四类模板测试:`SkillLearningPathServiceTest.java:176-265`,包括编程语言、框架、工具、通用能力、未知/空模板及分类不存在回退。 + - 空路径测试:`SkillLearningPathServiceTest.java:268-280`,断言 `total=0`、空列表且不访问分类仓储。 + - 重复调用测试:`SkillLearningPathServiceTest.java:282-309`,断言顺序、优先级和文本一致。 + - 本次修复仅修改测试文件;`SkillLearningPathService.java` 相对 `fca1160` 的生产差异为空。 + +5. 未发现 T8 实现: + - 当前 HEAD 仍为 `fca1160`。 + - 源码和测试目录未发现 `SkillCloudController`、`SkillLearningPathController`、`/skills` 词云或学习路径接口及对应安全配置变更。 + - 未发现超出 T7 范围的生产实现。 + +## 独立测试结果 + +环境:JDK 17.0.16、Maven 3.9.7。 + +执行命令: + +```powershell +$env:JAVA_HOME='D:\java\jdk' +& 'D:\maven\apache-maven-3.9.7\bin\mvn.cmd' '-Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest,SkillCloudQueryServiceTest,SkillMatchServiceTest,SkillLearningPathServiceTest' test +``` + +结果: + +```text +Tests run: 50, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +``` + +分类统计: + +```text +SkillTaxonomyMigrationTest: 2 passed +SkillCloudSeedDataIntegrationTest: 8 passed +SkillTaxonomyRepositoryTest: 6 passed +SkillCloudQueryServiceTest: 8 passed +SkillMatchServiceTest: 16 passed +SkillLearningPathServiceTest: 10 passed +``` + +## 剩余阻断项 + +无。T7-I1 已关闭,允许进入 T8 评审前置流程。 diff --git a/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T7.md b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T7.md new file mode 100644 index 0000000..67fd2ee --- /dev/null +++ b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-T7.md @@ -0,0 +1,72 @@ +# W2-T7 独立评审报告 + +## Verdict + +**FAIL** + +- 提交:`fca1160 feat: add skill learning path service` +- 评审对象:`SkillLearningPathService`、`SkillLearningPathItem`、`SkillLearningPathResponse`、`SkillLearningPathServiceTest` +- 阻断项:1 项 Important +- 结论:生产实现整体符合 T7 行为契约,但核心顺序行为缺少可证伪的回归测试,不能放行 T8。 + +## 独立测试 + +执行环境:JDK 17.0.16、Maven 3.9.7。 + +```powershell +$env:JAVA_HOME='D:\java\jdk' +& 'D:\maven\apache-maven-3.9.7\bin\mvn.cmd' '-Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest,SkillCloudQueryServiceTest,SkillMatchServiceTest,SkillLearningPathServiceTest' test +``` + +结果: + +```text +Tests run: 49, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +Total time: 1:00 min +``` + +## Important + +### T7-I1:测试无法阻止学习路径按名称重排 + +- 文件:`mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillLearningPathServiceTest.java:98-117` +- 相关实现:`mydreamjob-server/src/main/java/com/mydreamjob/skill/SkillLearningPathService.java:35-38` + +现有单测的 `missing` 顺序为: + +- `First` / `skillId=31` / `heat=1.00` +- `Second` / `skillId=32` / `heat=10.00` +- `Third` / `skillId=33` / `heat=5.00` + +期望输出保持该输入顺序。但输入的名称和 `skillId` 本身均为升序,因此把实现改成: + +```java +missingSkills.stream() + .sorted(Comparator.comparing(SkillCloudItem::nameZh)) + .map(...) +``` + +后,单测仍会通过。真实数据集成测试 `SkillLearningPathServiceTest.java:62-76` 使用职位 1,其当前 TOP10 全部为相同热度,名称顺序也已符合词云次级排序,因此同样无法发现该重排缺陷。 + +T7 的要求是严格复用 `SkillMatchService` 返回的 `missingSkills` 顺序,不自行排序;该回归会破坏 REQ-LP-2。测试需要使用不会被名称排序或 `skillId` 排序偶然保持一致的缺失技能顺序,最好增加多热度分组的真实数据场景。 + +## 已通过检查 + +- 学习路径只遍历 `matchResponse.missingSkills()`,未消费已命中技能;优先级从 1 连续生成。 +- 编程语言、框架、工具、通用四类模板均已实现;未知、空模板编码及分类不存在时回退通用模板。 +- 建议包含技能名,精确断言中未出现 URL、课程链接、讲师、价格、时长、完成度或 AI 内容。 +- 无缺口时返回 `total=0` 和空列表,且不访问分类仓储。 +- 重复调用返回相同顺序、优先级和建议。 +- `SkillLearningPathItem` 仅包含技能 ID、技能名、分类 ID、分类名、优先级和建议,不含隐私字段。 +- 服务使用只读事务;未发现持久化或外部服务调用。 +- 提交只新增 4 个指定文件,未包含 T8 及后续实现。 + +## Minor + +- `SkillLearningPathServiceTest.java:195-221` 覆盖了 null 模板编码、未知模板和分类不存在,但未直接覆盖 `skill.categoryId()==null`;实现已在 `SkillLearningPathService.java:57-58` 回退 GENERAL。 +- 外部内容主要通过精确模板断言和禁用词断言约束;生产类没有外部客户端依赖,因此本项不构成阻断。 + +## 未决问题 + +无。修复测试后需重新执行指定回归并复审。 \ No newline at end of file diff --git a/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-wave.md b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-wave.md new file mode 100644 index 0000000..d723c9f --- /dev/null +++ b/changes/n03-skill-cloud/.superpowers/sdd/reviews/W2-wave.md @@ -0,0 +1,175 @@ +# N03 SDD W2 整批独立评审报告 + +## 评审对象 + +- 范围:`b960036..f296df4` +- Wave:W2 +- 批次:B2 / T4–T7 +- 提交: + - `0c77824 feat: add skill taxonomy repositories` + - `52751e5 fix: constrain skill projections by occupation category` + - `e3eecfe feat: add skill cloud query service` + - `1666162 fix: expose skill source metadata in cloud items` + - `6eb30e7 feat: add personal skill match service` + - `fca1160 feat: add skill learning path service` + - `f296df4 test: harden learning path ordering regression` +- 评审依据:修订后的 `execution-contract.md`、`design.md`、`tasks.md`、三份 spec、V35/V36,以及 `W2-T4*`、`W2-T5*`、`W2-T6*`、`W2-T7*` 报告。 +- 环境:`JAVA_HOME=D:\java\jdk`,Maven `D:\maven\apache-maven-3.9.7\bin\mvn.cmd`,工作目录 `D:\1\zhi\zhilai\mydreamjob-server`。 + +## Verdict + +**PASS** + +W2/T4–T7 的领域与查询实现完整覆盖 B2 范围;词云、个人匹配、学习路径确实共享同一排名管道;未发现 Critical 或 Important 阻断项。W3/T8 可以开始,但必须先处理报告中的 Minor 项,并在 B3 补齐 HTTP、权限、响应序列化、隐私和真实 MySQL 验证。 + +## 测试结果 + +独立执行: + +```powershell +$env:JAVA_HOME='D:\java\jdk' +& 'D:\maven\apache-maven-3.9.7\bin\mvn.cmd' '-Dtest=SkillTaxonomyMigrationTest,SkillCloudSeedDataIntegrationTest,SkillTaxonomyRepositoryTest,SkillCloudQueryServiceTest,SkillMatchServiceTest,SkillLearningPathServiceTest' test +``` + +结果: + +```text +Tests run: 50, Failures: 0, Errors: 0, Skipped: 0 +BUILD SUCCESS +``` + +Surefire 分类: + +| 测试类 | 通过数 | +| --- | ---: | +| `SkillTaxonomyMigrationTest` | 2 | +| `SkillCloudSeedDataIntegrationTest` | 8 | +| `SkillTaxonomyRepositoryTest` | 6 | +| `SkillCloudQueryServiceTest` | 8 | +| `SkillMatchServiceTest` | 16 | +| `SkillLearningPathServiceTest` | 10 | + +补充静态检查: + +- `git diff --check b960036..f296df4` 通过。 +- `git log --oneline b960036..f296df4` 只包含上述 7 个 W2/T4–T7 提交。 +- 范围内实现文件集中在 `mydreamjob-server/src/main/java/com/mydreamjob/skill/**` 与对应测试;未发现 `SkillCloudController`、`SecurityConfig` 或 OpenAPI 变更。 + +## 实现与需求覆盖矩阵 + +| 需求 | W2 结论 | 主要实现与证据 | +| --- | --- | --- | +| REQ-SC-1 | 不属于 W2,待 B3 | 未实现 Controller/SecurityConfig;由 T8 负责。W2 只提供可被 Controller 调用的服务。 | +| REQ-SC-2 | 通过(服务层) | `SkillCloudQueryService.query` 校验必填、正整数、行业—职位关系、分类—职位关系,非法组合抛 `BusinessException(BAD_REQUEST)`,见 `SkillCloudQueryService.java:57-79`、`150-158`;测试见 `SkillCloudQueryServiceTest.java:238-264`。 | +| REQ-SC-3 | 通过 | `heat DESC, nameZh ASC, skillId ASC`,`null` heat 末位,见 `SkillCloudQueryService.java:28-40`、`86-99`、`120-144`;测试覆盖空 heat、同 heat、同名称 skillId,见 `SkillCloudQueryServiceTest.java:84-145`。 | +| REQ-SC-4 | 通过(B2) | 词云返回完整列表;匹配直接取 `min(10, actualSize)`,未另建排序,见 `SkillMatchService.java:53-55`;测试见 `SkillMatchServiceTest.java:85-118`。 | +| REQ-SC-5 | B2 通过,B3 待补 | B2 已返回 rank、技能身份、分类、heat、技能来源及热度来源字段,见 `SkillCloudItem.java:5-19`、`SkillCloudQueryService.java:102-117`;`ApiResponse` 包装属于 T8。 | +| REQ-SM-1 | 通过 | 匹配服务复用 `SkillCloudQueryService.query` 的结果前 `min(10, actualSize)` 项,见 `SkillMatchService.java:53-55`;测试见 `SkillMatchServiceTest.java:85-137`。 | +| REQ-SM-2 | B2 通过,B3 待补 | 服务只通过 `studentUserId` 查询 `student_profiles.skills_json`,缺失、空、损坏 JSON 均按空技能处理,见 `SkillMatchService.java:81-116`;JWT 身份提取必须由 T8 Controller 完成。 | +| REQ-SM-3 | 通过 | NFKC、trim、Locale.ROOT、Unicode 连字符、连续空白、句点、`+`/`#` 规则见 `SkillNameNormalizer.java:12-27`;nameZh/nameEn/skillCode/alias 和别名归属有隔离测试,见 `SkillMatchServiceTest.java:237-292`。 | +| REQ-SM-4 | 通过 | `round(matched * 100.0 / actualTopSize)`,空集合返回 0,重复学生技能通过 `Set` 去重,见 `SkillMatchService.java:54-77`、`100-110`;测试覆盖 1/3=33、全命中、空集合。 | +| REQ-SM-5 | 通过 | 命中/缺失列表按 TOP 顺序分别构造,并集等于实际 TOP10,见 `SkillMatchService.java:59-78`;测试见 `SkillMatchServiceTest.java:294-370`。 | +| REQ-LP-1 | 通过 | 学习路径完全由 `SkillMatchResponse.missingSkills()` 驱动,不重新加入已命中或 TOP10 外技能,见 `SkillLearningPathService.java:29-40`;测试见 `SkillLearningPathServiceTest.java:131-174`。 | +| REQ-LP-2 | 通过 | 直接沿用 missingSkills 顺序,优先级从 1 连续递增,见 `SkillLearningPathService.java:35-38`;真实 V36 多热度分组及名称/ID 乱序测试见 `SkillLearningPathServiceTest.java:88-121`。 | +| REQ-LP-3 | 通过 | 四类确定性模板及未知模板回退见 `SkillLearningPathService.java:56-93`;模板精确断言见 `SkillLearningPathServiceTest.java:176-265`。 | +| REQ-LP-4 | 通过(B2) | 路径项包含技能 ID、名称、分类、优先级和建议,建议文本不含链接、课程、价格、时长、完成度或 AI 内容,见 `SkillLearningPathItem.java:3-10`、`SkillLearningPathServiceTest.java:358-362`。 | + +## 关键检查结论 + +### 1. 三服务共享同一排名管道:通过 + +`SkillCloudQueryService` 是唯一排序实现: + +- 过滤:`SkillCloudQueryService.java:57-84` +- 排序:`SkillCloudQueryService.java:28-40`、`86-88` +- 排名:`SkillCloudQueryService.java:89-91` +- 来源映射:`SkillCloudQueryService.java:102-117` + +`SkillMatchService` 直接调用词云服务并截取前 `min(10, actualSize)` 项,没有复制排序逻辑,见 `SkillMatchService.java:53-55`。`SkillLearningPathService` 直接消费匹配服务的 `missingSkills`,没有重新排序,见 `SkillLearningPathService.java:33-38`。因此 TOP10、匹配缺口和学习路径的顺序语义一致。 + +### 2. 跨任务接口一致性:通过,存在命名待收口项 + +DTO 字段和顺序语义互相衔接: + +- `SkillCloudItem` 提供词云和匹配共用的完整技能项,见 `SkillCloudItem.java:5-19`。 +- `SkillMatchResponse` 返回 `matchScore`、`matchedCount`、`totalCount`、`matchedSkills`、`missingSkills`,见 `SkillMatchResponse.java:5-15`。 +- `SkillLearningPathResponse` 返回 `total`、`items`;`SkillLearningPathItem` 返回 `priority`、技能、分类和建议,见 `SkillLearningPathResponse.java:5-11`、`SkillLearningPathItem.java:3-10`。 + +Minor M1:设计文档 `design.md:131` 使用概念字段名 `heatSource`,而实现采用扁平字段 `heatSourceCode`、`heatElementId`、`heatProxy`,见 `SkillCloudItem.java:15-18`。这不是 B2 行为错误,但 T8 必须在 OpenAPI、接口文档和前端契约中明确最终 JSON 命名,避免文档与运行时漂移。 + +### 3. 领域边界、事务、空值和异常:通过 + +- 三个业务服务均为 `@Transactional(readOnly = true)`,没有写操作,见三个服务类的类级注解。 +- 分类—职位约束同时存在于服务校验和 Repository 投影查询中;跨职位分类错配测试见 `SkillTaxonomyRepositoryTest.java:123-152`。 +- 空 heat 保留并末位排序:`SkillCloudQueryService.java:120-131`。 +- 空结果:词云返回 `total=0, items=[]`;匹配返回 `matchScore=0`、空命中/缺失;学习路径返回空列表,见对应测试。 +- 损坏 JSON 被捕获并仅记录异常类型和 profile ID,不输出原始 JSON,见 `SkillMatchService.java:91-114`。 +- 学习路径未知/空/缺失模板回退通用模板,见 `SkillLearningPathService.java:56-65`、`84-93`。 + +### 4. 确定性与性能:通过,Minor N+1 + +- 排序使用 `BigDecimal.compareTo`、`nameZh`、`skillId` 全序比较,重复请求结果确定。 +- 每个缺失技能最多一次分类模板查询,构建学习路径时最多 10 次查询,见 `SkillLearningPathService.java:61-64`。 + +Minor M2:这是有界的 N+1。当前任务是代表性小词库、最多 10 个路径项,不构成 W2 阻断;如果后续词库或 TOP 上限扩大,应在 T8/T10 或后续优化中改为一次批量读取模板。 + +### 5. 测试可证伪性:通过,有少量 Minor 缺口 + +已覆盖用户要求的关键反例: + +- 跨职位分类错配:`SkillTaxonomyRepositoryTest.java:123-152`、`SkillMatchServiceTest.java:347-370`。 +- 排序错误:`SkillCloudQueryServiceTest.java:84-145` 覆盖热度、同名/同热度的 skillId 稳定排序。 +- 名称、别名和别名归属遗漏:`SkillMatchServiceTest.java:237-292`。 +- 分母错误:`SkillMatchServiceTest.java:120-137`。 +- 优先级重排:`SkillLearningPathServiceTest.java:88-174`。 +- 模板遗漏:`SkillLearningPathServiceTest.java:176-265`。 +- 空路径:`SkillLearningPathServiceTest.java:268-280`。 + +Minor M3:B2 测试未直接覆盖 `SkillLearningPathService.resolveTemplate` 在 `skill.categoryId()==null` 时的通用模板分支;也未通过完整序列化验证所有响应对象不含原始 `skills_json`。生产 DTO 当前不含隐私字段,且 `SkillLearningPathServiceTest.java:358-362` 已约束建议文本;T8/T9 的 MockMvc/端到端测试必须补齐响应级隐私断言。 + +### 6. W2 未越过 T8 边界:通过 + +`git diff --name-only b960036..f296df4` 未出现: + +- `SkillCloudController` 或任何 `/skills/**` Controller; +- `SecurityConfig` 变更; +- OpenAPI 文件; +- `/api/v1/skills/*` 运行时端点。 + +因此 W2 没有实现 T8 的接口、权限或 OpenAPI 工作,边界符合 `tasks.md:24-25` 的后续安排。 + +## 严重级别与发现 + +### Critical + +无。 + +### Important + +无。 + +### Minor + +- **M1:设计中的 `heatSource` 与实际扁平来源字段未最终对齐。** + - 位置:`changes/n03-skill-cloud/design.md:131`;`mydreamjob-server/src/main/java/com/mydreamjob/skill/dto/SkillCloudItem.java:15-18`。 + - 处理方向:T8 明确选择扁平 `heatSourceCode`/`heatElementId`/`heatProxy` 或嵌套 `heatSource`,并同步 OpenAPI、接口文档和前端示例。 + +- **M2:学习路径模板查询是有界 N+1。** + - 位置:`mydreamjob-server/src/main/java/com/mydreamjob/skill/SkillLearningPathService.java:61-64`。 + - 处理方向:当前最多 10 次查询,不阻断;若词库或 TOP 上限扩大,应批量读取 `(categoryId, occupationId)` 模板映射。 + +- **M3:B2 测试未直接覆盖空 `categoryId` 模板回退和完整响应序列化隐私断言。** + - 位置:`mydreamjob-server/src/test/java/com/mydreamjob/skill/SkillLearningPathServiceTest.java:240-265`、`358-362`。 + - 处理方向:T8 Controller 测试必须验证三个接口的响应不含 `skills_json`、手机号、邮箱和简历内容;可同时补 `categoryId=null` 的 Service 单测。 + +## 未决问题 + +1. `SkillCloudItem` 热度来源字段的最终 JSON 形态尚未在设计中收口,需在 T8 同时对齐实现、OpenAPI 和前端联调文档。 +2. B2 只验证 H2 `MODE=MySQL` 与 Service 层异常类型;真实 MySQL 迁移、HTTP 401/403/400、JWT 到 `studentUserId` 的绑定和 `ApiResponse` 包装仍属于 B3。 +3. B2 没有执行 `mvn clean verify` 或静态 OpenAPI 校验;这些属于 T9/T10 的退出条件,当前不作为 W2 阻断。 + +## 是否放行 W3 + +**放行。** + +W2 的领域行为、排名管道、个人匹配、学习路径和可证伪测试已达到 B2 完成定义。允许进入 W3/T8;W3 开始前必须在实现 Controller/SecurityConfig/OpenAPI 时处理 M1,并在 T8–T10 验证上述 HTTP、权限、隐私和真实数据库边界。