From 4b795a422488332faa9e2356f47fe57f98990671 Mon Sep 17 00:00:00 2001 From: QiuSW <105186638@qq.com> Date: Mon, 27 Jul 2026 22:31:17 +0800 Subject: [PATCH] fix(t212): preserve ranked candidate identity --- .../java/com/roubao/autopilot/MainActivity.kt | 256 +++++++++++------- .../procurement/ExecutionResultOutbox.kt | 50 ++++ .../vlm/CandidateEvaluationModels.kt | 31 ++- .../ExecutionCandidateIdentityPolicyTest.kt | 87 ++++++ .../vlm/CandidateTopFivePolicyTest.kt | 33 ++- docs/current-state.md | 18 +- docs/tasks/T-212.md | 22 +- 7 files changed, 382 insertions(+), 115 deletions(-) create mode 100644 android-buyer/app/src/test/java/com/roubao/autopilot/procurement/ExecutionCandidateIdentityPolicyTest.kt diff --git a/android-buyer/app/src/main/java/com/roubao/autopilot/MainActivity.kt b/android-buyer/app/src/main/java/com/roubao/autopilot/MainActivity.kt index 648e2cd..479f88c 100644 --- a/android-buyer/app/src/main/java/com/roubao/autopilot/MainActivity.kt +++ b/android-buyer/app/src/main/java/com/roubao/autopilot/MainActivity.kt @@ -93,12 +93,15 @@ import com.roubao.autopilot.procurement.LoginInput import com.roubao.autopilot.procurement.ExecutionCandidateBatchDraft import com.roubao.autopilot.procurement.ExecutionCandidateDraft import com.roubao.autopilot.procurement.ExecutionCandidateEvaluation +import com.roubao.autopilot.procurement.ExecutionCandidateIdentityPolicy +import com.roubao.autopilot.procurement.ExecutionCandidateSourceIdentity import com.roubao.autopilot.procurement.ExecutionHardConstraintEvaluation import com.roubao.autopilot.procurement.ExecutionRecommendation import com.roubao.autopilot.procurement.ExecutionEvidenceDraft import com.roubao.autopilot.procurement.ExecutionMode import com.roubao.autopilot.procurement.ExecutionProvenanceSnapshot import com.roubao.autopilot.procurement.ProcurementRepository +import com.roubao.autopilot.procurement.RankedExecutionCandidateDraft import com.roubao.autopilot.task.RequirementProbeFixture import com.roubao.autopilot.workflow.WorkflowReport import com.roubao.autopilot.workflow.WorkflowRunner @@ -144,8 +147,8 @@ class MainActivity : ComponentActivity() { private val candidateReviewBatch = mutableStateOf(null) private val candidateEvaluationFailureCode = mutableStateOf(null) - private val taskCandidateDrafts = - mutableStateOf>(emptyList()) + private val taskCandidateDrafts = + mutableStateOf>(emptyList()) private var searchProbeRunner: WorkflowRunner? = null private var searchProbeJob: Job? = null private var requirementProbeJob: Job? = null @@ -971,16 +974,19 @@ class MainActivity : ComponentActivity() { procurementRepository.activeExecutionMode() == ExecutionMode.AI_ASSISTED ) { - val selected = CandidateTopFivePolicy.select( + val ranked = CandidateTopFivePolicy.rank( result.batch ) val evidenceByOrdinal = validated.associateBy { it.ordinal } - val drafts = selected.mapIndexed { index, assessment -> + val drafts = ranked.map { rankedCandidate -> + val assessment = rankedCandidate.assessment ExecutionCandidateDraft( - ordinal = index + 1, - title = "拼多多图片候选 ${assessment.ordinal}", + ordinal = rankedCandidate.rankedOrdinal, + title = + "拼多多图片候选 " + + rankedCandidate.sourceOrdinal, skuText = assessment.hardConstraintResults .joinToString(" / ") { "${it.kind.name}:${it.expected}" @@ -1008,52 +1014,82 @@ class MainActivity : ComponentActivity() { ) ) } - val selectedEvidence = selected.mapIndexed { - index, - assessment -> + val selectedEvidence = ranked.map { rankedCandidate -> val candidate = requireNotNull( - evidenceByOrdinal[assessment.ordinal] + evidenceByOrdinal[ + rankedCandidate.sourceOrdinal + ] + ) + require( + candidate.sha256 == + rankedCandidate.evidenceSha256 ) ExecutionEvidenceDraft( - ordinal = index + 1, + ordinal = rankedCandidate.rankedOrdinal, pngBytes = candidate.pngBytes, sha256 = candidate.sha256 ) } - taskCandidateDrafts.value = - procurementRepository.queueCandidateBatch( - ExecutionCandidateBatchDraft( + val queued = procurementRepository.queueCandidateBatch( + ExecutionCandidateBatchDraft( + mode = ExecutionMode.AI_ASSISTED, + searchQuery = + candidateSearchKeyword.value + ?: PDD_IMAGE_SEARCH_AUDIT_QUERY, + provenance = ExecutionProvenanceSnapshot( mode = ExecutionMode.AI_ASSISTED, - searchQuery = - candidateSearchKeyword.value - ?: PDD_IMAGE_SEARCH_AUDIT_QUERY, - provenance = ExecutionProvenanceSnapshot( - mode = ExecutionMode.AI_ASSISTED, - providerId = result.batch.providerId, - model = result.batch.model, - promptVersion = - CANDIDATE_EVALUATION_PROMPT_VERSION, - schemaVersion = - CANDIDATE_EVALUATION_SCHEMA_VERSION, - referenceImageSha256 = - result.batch - .requirementReferenceImageSha256 - ), - candidates = drafts, - recommendation = drafts.firstOrNull()?.let { - ExecutionRecommendation( - candidateOrdinal = 1, - policyVersion = - "sku-hard-constraints-v1", - reasons = listOf( - "SKU颜色和尺码硬约束均确认匹配", - "按评估分和置信度排序" - ) - ) - } + providerId = result.batch.providerId, + model = result.batch.model, + promptVersion = + CANDIDATE_EVALUATION_PROMPT_VERSION, + schemaVersion = + CANDIDATE_EVALUATION_SCHEMA_VERSION, + referenceImageSha256 = + result.batch + .requirementReferenceImageSha256 ), - selectedEvidence - ).orEmpty() + candidates = drafts, + recommendation = drafts.firstOrNull()?.let { + ExecutionRecommendation( + candidateOrdinal = 1, + policyVersion = + "sku-hard-constraints-v1", + reasons = listOf( + "SKU颜色和尺码硬约束均确认匹配", + "按评估分和置信度排序" + ) + ) + } + ), + selectedEvidence + ) ?: run { + setCandidateEvaluationFailure( + CandidateEvaluationFailureCode + .EVIDENCE_INVALID + ) + return@launch + } + taskCandidateDrafts.value = runCatching { + ExecutionCandidateIdentityPolicy.bind( + identities = ranked.map { rankedCandidate -> + ExecutionCandidateSourceIdentity( + sourceOrdinal = + rankedCandidate.sourceOrdinal, + rankedOrdinal = + rankedCandidate.rankedOrdinal, + evidenceSha256 = + rankedCandidate.evidenceSha256 + ) + }, + candidates = queued + ) + }.getOrElse { + setCandidateEvaluationFailure( + CandidateEvaluationFailureCode + .EVIDENCE_INVALID + ) + return@launch + } } candidateReviewBatch.value = result.batch candidateEvaluationState.value = when ( @@ -1084,37 +1120,54 @@ class MainActivity : ComponentActivity() { candidateEvaluationJob?.cancel() } - private suspend fun queueManualProcurementCandidates( - taskTitle: String, - searchQuery: String, - evidence: List - ) { - val validated = candidateEvidenceSource.load(evidence).getOrNull() ?: return - taskCandidateDrafts.value = procurementRepository.queueCandidateBatch( - ExecutionCandidateBatchDraft( - mode = ExecutionMode.MANUAL_FIRST, - searchQuery = searchQuery, - provenance = null, - candidates = validated.map { candidate -> - ExecutionCandidateDraft( - ordinal = candidate.ordinal, - title = "$taskTitle 候选 ${candidate.ordinal}", - evidenceLocalIDs = emptyList() - ) - } - ), - validated.map { candidate -> - ExecutionEvidenceDraft( - ordinal = candidate.ordinal, - pngBytes = candidate.pngBytes, - sha256 = candidate.sha256 - ) - } - ).orEmpty() - if (taskCandidateDrafts.value.isNotEmpty()) { - candidateEvaluationState.value = CandidateEvaluationState.MANUAL_REVIEW - } - } + private suspend fun queueManualProcurementCandidates( + taskTitle: String, + searchQuery: String, + evidence: List + ) { + val validated = candidateEvidenceSource.load(evidence).getOrNull() ?: return + val queued = procurementRepository.queueCandidateBatch( + ExecutionCandidateBatchDraft( + mode = ExecutionMode.MANUAL_FIRST, + searchQuery = searchQuery, + provenance = null, + candidates = validated.map { candidate -> + ExecutionCandidateDraft( + ordinal = candidate.ordinal, + title = "$taskTitle 候选 ${candidate.ordinal}", + evidenceLocalIDs = emptyList() + ) + } + ), + validated.map { candidate -> + ExecutionEvidenceDraft( + ordinal = candidate.ordinal, + pngBytes = candidate.pngBytes, + sha256 = candidate.sha256 + ) + } + ) ?: return + taskCandidateDrafts.value = runCatching { + ExecutionCandidateIdentityPolicy.bind( + identities = validated.map { candidate -> + ExecutionCandidateSourceIdentity( + sourceOrdinal = candidate.ordinal, + rankedOrdinal = candidate.ordinal, + evidenceSha256 = candidate.sha256 + ) + }, + candidates = queued + ) + }.getOrElse { + setCandidateEvaluationFailure( + CandidateEvaluationFailureCode.EVIDENCE_INVALID + ) + return + } + if (taskCandidateDrafts.value.isNotEmpty()) { + candidateEvaluationState.value = CandidateEvaluationState.MANUAL_REVIEW + } + } private fun manualSearchQuery(title: String, sku: String): String = listOf(title.trim(), sku.trim()) @@ -1127,27 +1180,39 @@ class MainActivity : ComponentActivity() { currentState = candidateEvaluationState.value, batch = candidateReviewBatch.value ) - if (candidateEvaluationState.value != CandidateEvaluationState.HUMAN_ACCEPTED && - taskCandidateDrafts.value.isNotEmpty() - ) { - candidateEvaluationState.value = CandidateEvaluationState.HUMAN_ACCEPTED - } - if (candidateEvaluationState.value == CandidateEvaluationState.HUMAN_ACCEPTED) { - val recommendedOrdinal = candidateReviewBatch.value - ?.recommendedCandidateOrdinal - val candidate = taskCandidateDrafts.value.firstOrNull { - it.ordinal == recommendedOrdinal - } ?: taskCandidateDrafts.value.firstOrNull() - if (candidate != null && procurementRepository.currentProbeTask() != null) { - lifecycleScope.launch { - procurementRepository.completeExecution( - outcome = "CANDIDATE_ACCEPTED", - operatorReason = operatorReason, - candidate = candidate - ) - } - } - } + if ( + candidateEvaluationState.value != CandidateEvaluationState.HUMAN_ACCEPTED && + taskCandidateDrafts.value.isNotEmpty() + ) { + candidateEvaluationState.value = CandidateEvaluationState.HUMAN_ACCEPTED + } + if ( + candidateEvaluationState.value == + CandidateEvaluationState.HUMAN_ACCEPTED + ) { + val candidate = runCatching { + ExecutionCandidateIdentityPolicy.recommended( + taskCandidateDrafts.value + ) + }.getOrElse { + setCandidateEvaluationFailure( + CandidateEvaluationFailureCode.EVIDENCE_INVALID + ) + return + } + if ( + candidate != null && + procurementRepository.currentProbeTask() != null + ) { + lifecycleScope.launch { + procurementRepository.completeExecution( + outcome = "CANDIDATE_ACCEPTED", + operatorReason = operatorReason, + candidate = candidate + ) + } + } + } } private fun rejectCandidateReview(operatorReason: String) { @@ -1183,6 +1248,7 @@ class MainActivity : ComponentActivity() { candidateReviewBatch.value = null candidateEvaluationFailureCode.value = null candidateEvaluationState.value = CandidateEvaluationState.IDLE + taskCandidateDrafts.value = emptyList() } private fun checkShizukuPermission(): Boolean { diff --git a/android-buyer/app/src/main/java/com/roubao/autopilot/procurement/ExecutionResultOutbox.kt b/android-buyer/app/src/main/java/com/roubao/autopilot/procurement/ExecutionResultOutbox.kt index 59db726..92999f4 100644 --- a/android-buyer/app/src/main/java/com/roubao/autopilot/procurement/ExecutionResultOutbox.kt +++ b/android-buyer/app/src/main/java/com/roubao/autopilot/procurement/ExecutionResultOutbox.kt @@ -19,6 +19,56 @@ data class ExecutionCandidateDraft( val evaluation: ExecutionCandidateEvaluation? = null ) +data class ExecutionCandidateSourceIdentity( + val sourceOrdinal: Int, + val rankedOrdinal: Int, + val evidenceSha256: String +) + +data class RankedExecutionCandidateDraft( + val identity: ExecutionCandidateSourceIdentity, + val candidate: ExecutionCandidateDraft +) + +object ExecutionCandidateIdentityPolicy { + private val sha256Pattern = Regex("^[0-9a-f]{64}$") + + fun bind( + identities: List, + candidates: List + ): List { + require(identities.size == candidates.size) + require( + identities.map { it.rankedOrdinal } == + (1..identities.size).toList() + ) + require(identities.all { it.sourceOrdinal > 0 }) + require(identities.map { it.sourceOrdinal }.distinct().size == identities.size) + require(identities.all { sha256Pattern.matches(it.evidenceSha256) }) + val candidatesByOrdinal = candidates.associateBy { it.ordinal } + require(candidatesByOrdinal.size == candidates.size) + return identities.map { identity -> + RankedExecutionCandidateDraft( + identity = identity, + candidate = requireNotNull( + candidatesByOrdinal[identity.rankedOrdinal] + ) + ) + } + } + + fun recommended( + candidates: List + ): ExecutionCandidateDraft? = + when { + candidates.isEmpty() -> null + else -> candidates.single { + it.identity.rankedOrdinal == 1 && + it.candidate.ordinal == 1 + }.candidate + } +} + data class ExecutionCandidateEvaluation( val decision: String, val score: Double, diff --git a/android-buyer/app/src/main/java/com/roubao/autopilot/vlm/CandidateEvaluationModels.kt b/android-buyer/app/src/main/java/com/roubao/autopilot/vlm/CandidateEvaluationModels.kt index e3c1ad5..6edbd36 100644 --- a/android-buyer/app/src/main/java/com/roubao/autopilot/vlm/CandidateEvaluationModels.kt +++ b/android-buyer/app/src/main/java/com/roubao/autopilot/vlm/CandidateEvaluationModels.kt @@ -59,6 +59,19 @@ data class CandidateAssessment( val hardConstraintResults: List = emptyList() ) +data class RankedCandidateAssessment( + val sourceOrdinal: Int, + val rankedOrdinal: Int, + val evidenceSha256: String, + val assessment: CandidateAssessment +) { + init { + require(sourceOrdinal == assessment.ordinal) + require(rankedOrdinal > 0) + require(evidenceSha256 == assessment.evidenceSha256) + } +} + enum class CandidateBatchConclusion { SUGGESTED, NO_MATCH, @@ -166,10 +179,10 @@ object CandidateHumanReviewPolicy { } object CandidateTopFivePolicy { - fun select( + fun rank( batch: CandidateReviewBatch, limit: Int = 5 - ): List { + ): List { require(limit in 1..5) return batch.assessments .filter { assessment -> @@ -188,5 +201,19 @@ object CandidateTopFivePolicy { .thenBy { it.ordinal } ) .take(limit) + .mapIndexed { index, assessment -> + RankedCandidateAssessment( + sourceOrdinal = assessment.ordinal, + rankedOrdinal = index + 1, + evidenceSha256 = assessment.evidenceSha256, + assessment = assessment + ) + } } + + fun select( + batch: CandidateReviewBatch, + limit: Int = 5 + ): List = + rank(batch, limit).map { it.assessment } } diff --git a/android-buyer/app/src/test/java/com/roubao/autopilot/procurement/ExecutionCandidateIdentityPolicyTest.kt b/android-buyer/app/src/test/java/com/roubao/autopilot/procurement/ExecutionCandidateIdentityPolicyTest.kt new file mode 100644 index 0000000..59d1d42 --- /dev/null +++ b/android-buyer/app/src/test/java/com/roubao/autopilot/procurement/ExecutionCandidateIdentityPolicyTest.kt @@ -0,0 +1,87 @@ +package com.roubao.autopilot.procurement + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertThrows +import org.junit.Test + +class ExecutionCandidateIdentityPolicyTest { + @Test + fun `recommended candidate uses ranked first instead of source ordinal`() { + val bindings = ExecutionCandidateIdentityPolicy.bind( + identities = listOf( + identity(sourceOrdinal = 3, rankedOrdinal = 1), + identity(sourceOrdinal = 1, rankedOrdinal = 2), + identity(sourceOrdinal = 2, rankedOrdinal = 3) + ), + candidates = listOf(candidate(1), candidate(2), candidate(3)) + ) + + val recommended = ExecutionCandidateIdentityPolicy.recommended(bindings) + + assertEquals(1, recommended?.ordinal) + assertEquals(3, bindings.first().identity.sourceOrdinal) + assertEquals("evidence-1", recommended?.evidenceLocalIDs?.single()) + } + + @Test + fun `filtered source ordinals keep continuous ranked identities`() { + val bindings = ExecutionCandidateIdentityPolicy.bind( + identities = listOf( + identity(sourceOrdinal = 2, rankedOrdinal = 1), + identity(sourceOrdinal = 5, rankedOrdinal = 2) + ), + candidates = listOf(candidate(1), candidate(2)) + ) + + assertEquals( + listOf(2, 5), + bindings.map { it.identity.sourceOrdinal } + ) + assertEquals( + listOf(1, 2), + bindings.map { it.candidate.ordinal } + ) + } + + @Test + fun `empty candidates have no recommendation`() { + assertNull( + ExecutionCandidateIdentityPolicy.recommended( + ExecutionCandidateIdentityPolicy.bind( + identities = emptyList(), + candidates = emptyList() + ) + ) + ) + } + + @Test + fun `missing ranked ordinal is rejected instead of falling back`() { + assertThrows(IllegalArgumentException::class.java) { + ExecutionCandidateIdentityPolicy.bind( + identities = listOf( + identity(sourceOrdinal = 3, rankedOrdinal = 1) + ), + candidates = listOf(candidate(2)) + ) + } + } + + private fun identity( + sourceOrdinal: Int, + rankedOrdinal: Int + ): ExecutionCandidateSourceIdentity = + ExecutionCandidateSourceIdentity( + sourceOrdinal = sourceOrdinal, + rankedOrdinal = rankedOrdinal, + evidenceSha256 = sourceOrdinal.toString(16).padStart(64, '0') + ) + + private fun candidate(ordinal: Int): ExecutionCandidateDraft = + ExecutionCandidateDraft( + ordinal = ordinal, + title = "candidate-$ordinal", + evidenceLocalIDs = listOf("evidence-$ordinal") + ) +} diff --git a/android-buyer/app/src/test/java/com/roubao/autopilot/vlm/CandidateTopFivePolicyTest.kt b/android-buyer/app/src/test/java/com/roubao/autopilot/vlm/CandidateTopFivePolicyTest.kt index 10420b6..7d2e35e 100644 --- a/android-buyer/app/src/test/java/com/roubao/autopilot/vlm/CandidateTopFivePolicyTest.kt +++ b/android-buyer/app/src/test/java/com/roubao/autopilot/vlm/CandidateTopFivePolicyTest.kt @@ -6,7 +6,7 @@ import org.junit.Test class CandidateTopFivePolicyTest { @Test fun `sorts eligible matches and limits result to five`() { - val selected = CandidateTopFivePolicy.select( + val ranked = CandidateTopFivePolicy.rank( batch( assessment(1, score = 0.80, confidence = 0.90), assessment(2, score = 0.90, confidence = 0.80), @@ -24,7 +24,18 @@ class CandidateTopFivePolicyTest { ) ) - assertEquals(listOf(3, 2, 5, 6, 7), selected.map { it.ordinal }) + assertEquals( + listOf(3, 2, 5, 6, 7), + ranked.map { it.sourceOrdinal } + ) + assertEquals( + listOf(1, 2, 3, 4, 5), + ranked.map { it.rankedOrdinal } + ) + assertEquals( + ranked.map { it.assessment.evidenceSha256 }, + ranked.map { it.evidenceSha256 } + ) } @Test @@ -45,6 +56,24 @@ class CandidateTopFivePolicyTest { assertEquals(listOf(1), selected.map { it.ordinal }) } + @Test + fun `original third candidate becomes stable first ranked candidate`() { + val ranked = CandidateTopFivePolicy.rank( + batch( + assessment(1, score = 0.80, confidence = 0.90), + assessment(2, score = 0.85, confidence = 0.90), + assessment(3, score = 0.95, confidence = 0.95) + ) + ) + + assertEquals(3, ranked.first().sourceOrdinal) + assertEquals(1, ranked.first().rankedOrdinal) + assertEquals( + 3.toString(16).padStart(64, '0'), + ranked.first().evidenceSha256 + ) + } + private fun batch(vararg assessments: CandidateAssessment): CandidateReviewBatch = CandidateReviewBatch( assessments = assessments.toList(), diff --git a/docs/current-state.md b/docs/current-state.md index 694f463..9f9b0e6 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -5,10 +5,9 @@ ## 当前快照 - 日期:2026-07-27 -- 阶段:T-211 参考图召回与 SKU 硬匹配完成;下一步 T-212 修复候选重排映射, - 随后 T-213 只读核验拼多多规格弹窗 +- 阶段:T-212 候选重排身份映射完成;下一步 T-213 只读核验拼多多规格弹窗 - Git:当前分支为 `main`;T-001 至 T-004、T-101 至 T-104、T-201 至 T-207、T-209、 - T-210、T-211 均已纳入 Git 历史 + T-210、T-211、T-212 均已纳入 Git 历史 - 生产代码:`android-buyer/` 已接入 Roubao Android 源码 - Android:固定 `main@c8a6d7f03422eb01744b01f3ee77bf7757741f7e`;MIT 许可证已保留 @@ -18,8 +17,8 @@ - 本机 Android 工具:JDK 17.0.13、Command-line Tools 22.0、SDK 34、 Build Tools 34.0.0、Platform Tools/ADB 37.0.0;用户级 SDK 环境变量已设置 - Android Studio:未安装;`winget` 静默安装卡住后已终止,不阻塞命令行构建 -- 测试:T-211 运行 `:app:testDebugUnitTest` 和 `:app:assembleDebug` 通过,20 个 suite - 共 108 个测试、0 失败;最新 Debug APK 已安装到 PKG110 +- 测试:T-212 运行 `:app:testDebugUnitTest` 和 `:app:assembleDebug` 通过,21 个 suite + 共 113 个测试、0 失败;T-211 Debug APK 已安装到 PKG110 - 后端测试:T-211 运行 `GOTOOLCHAIN=local go test -count=1 ./...` 和 `go vet ./...` 通过;T-207 的全包 race 与 migration 验证继续有效 - 原型:4 个管理 Web 页面和 7 个 Android 页面均可离线独立打开;Playwright @@ -108,7 +107,7 @@ | `docs/tasks/T-209.md` | DONE | 登录失败保留采购员和设备输入以便直接重试 | | `docs/tasks/T-210.md` | DONE | 兼容拼多多 8.17 搜索结果页与长词省略显示 | | `docs/tasks/T-211.md` | DONE | 参考图召回、SKU 颜色尺码硬匹配与 0..5 候选回传 | -| `docs/tasks/T-212.md` | TODO | 修复重排候选、推荐、证据与人工接受的身份映射 | +| `docs/tasks/T-212.md` | DONE | 修复重排候选、推荐、证据与人工接受的身份映射 | | `docs/tasks/T-213.md` | TODO | 只读采集拼多多规格弹窗中的颜色和尺码证据 | | `docs/design/` | 已确认 | T-202 原型索引、4 个管理页和 7 个 Android 页面 | | `deepseek总结.txt` | 已有 | 历史讨论摘要,不是正式需求权威 | @@ -120,9 +119,10 @@ ## 任务摘要 -- 已完成:T-001 至 T-004、T-101 至 T-104、T-201 至 T-207、T-209、T-210、T-211。 -- 正在进行:T-212 修复 Top 5 候选 ordinal 与证据映射。 -- 下一个可领取任务:无;T-213 依赖 T-212 完成。 +- 已完成:T-001 至 T-004、T-101 至 T-104、T-201 至 T-207、T-209、T-210、T-211、 + T-212。 +- 正在进行:无。 +- 下一个可领取任务:T-213 拼多多规格弹窗只读核验。 - 后置任务:T-213 拼多多规格弹窗只读核验;完成后再做 T-208 候选决策数据与人工 理由闭环。 diff --git a/docs/tasks/T-212.md b/docs/tasks/T-212.md index 81be64d..16f1166 100644 --- a/docs/tasks/T-212.md +++ b/docs/tasks/T-212.md @@ -4,7 +4,7 @@ title: 修复 Top 5 候选 ordinal 与证据映射 phase: 2 deps: - T-211 -status: DOING +status: DONE created: 2026-07-27 context_ref: edfee2c work_branch: null @@ -44,12 +44,12 @@ VLM 原始候选 ordinal 查找已经重排的 `ExecutionCandidateDraft`。当 ## 验收要点 -- [ ] 原始候选 3 得分最高时,回传候选 1、推荐项和人工接受终态都引用原候选 3 的证据。 -- [ ] 过滤后只剩原始候选 2、5 时,回传 ordinal 为 1、2,证据分别保持 2、5 的来源。 -- [ ] 相同分数按置信度和原始曝光顺序稳定排序,重试不会改变映射。 -- [ ] 空候选、单候选、拒绝和无匹配路径不产生越界或错误 fallback。 -- [ ] 后端 schema v2 的推荐首项、连续 ordinal 和证据资产校验继续通过。 -- [ ] Android 单元测试和 Debug 构建通过;人工接受仍保持 +- [x] 原始候选 3 得分最高时,回传候选 1、推荐项和人工接受终态都引用原候选 3 的证据。 +- [x] 过滤后只剩原始候选 2、5 时,回传 ordinal 为 1、2,证据分别保持 2、5 的来源。 +- [x] 相同分数按置信度和原始曝光顺序稳定排序,重试不会改变映射。 +- [x] 空候选、单候选、拒绝和无匹配路径不产生越界或错误 fallback。 +- [x] 后端 schema v2 的推荐首项、连续 ordinal 和证据资产校验继续通过。 +- [x] Android 单元测试和 Debug 构建通过;人工接受仍保持 `order_submitted=false`。 ## 边界 @@ -64,3 +64,11 @@ VLM 原始候选 ordinal 查找已经重排的 `ExecutionCandidateDraft`。当 采集,避免把错误候选写入后续优化数据。 - 2026-07-27:基于 `1d9dd08` 领取任务,先建立可单测的显式映射模型,再接入候选 批次和人工接受路径。 +- 2026-07-27:新增 `RankedCandidateAssessment` 和 + `ExecutionCandidateSourceIdentity`,显式保存原始 ordinal、重排 ordinal 和证据 + SHA-256。候选批次、推荐和人工接受均从同一映射生成;删除按原始 ordinal 查找和 + “找不到就取第一个”的 fallback,映射不一致时进入受控证据失败。 +- 2026-07-27:新增原候选 3 重排为 1、过滤后原候选 2/5 重排为 1/2、空候选和 + 缺失 ordinal 回归测试。`.\gradlew.bat :app:testDebugUnitTest :app:assembleDebug + --no-daemon` 通过,21 个 suite 共 113 个测试、0 失败; + `GOTOOLCHAIN=local go test -count=1 ./...` 通过。