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