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.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<CandidateReviewBatch?>(null)
|
||||
private val candidateEvaluationFailureCode =
|
||||
mutableStateOf<CandidateEvaluationFailureCode?>(null)
|
||||
private val taskCandidateDrafts =
|
||||
mutableStateOf<List<ExecutionCandidateDraft>>(emptyList())
|
||||
private val taskCandidateDrafts =
|
||||
mutableStateOf<List<RankedExecutionCandidateDraft>>(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<PinduoduoCandidateEvidence>
|
||||
) {
|
||||
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<PinduoduoCandidateEvidence>
|
||||
) {
|
||||
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 {
|
||||
|
||||
+50
@@ -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<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(
|
||||
val decision: String,
|
||||
val score: Double,
|
||||
|
||||
+29
-2
@@ -59,6 +59,19 @@ data class CandidateAssessment(
|
||||
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 {
|
||||
SUGGESTED,
|
||||
NO_MATCH,
|
||||
@@ -166,10 +179,10 @@ object CandidateHumanReviewPolicy {
|
||||
}
|
||||
|
||||
object CandidateTopFivePolicy {
|
||||
fun select(
|
||||
fun rank(
|
||||
batch: CandidateReviewBatch,
|
||||
limit: Int = 5
|
||||
): List<CandidateAssessment> {
|
||||
): List<RankedCandidateAssessment> {
|
||||
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<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 {
|
||||
@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(),
|
||||
|
||||
Reference in New Issue
Block a user