package usecase import ( "context" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "io" "net/url" "regexp" "strconv" "strings" "time" "unicode/utf8" "cmroubao/backend-api/internal/domain" ) const ( executionResultEventsOperation = "EVENTS" executionResultEvidenceOperation = "EVIDENCE" executionResultCandidatesOperation = "CANDIDATES" executionResultHumanReviewOperation = "HUMAN_REVIEW" executionResultCompleteOperation = "COMPLETE" executionResultFailOperation = "FAIL" manualFirstMode = "MANUAL_FIRST" aiAssistedMode = "AI_ASSISTED" ) type ExecutionResultService struct { repository ExecutionResultRepository store ReferenceImageStore clock Clock ids IDGenerator } type ExecutionResultIdentity struct { UserID string DeviceID string TaskID string ExecutionID string ClaimGeneration int64 ClaimToken string IdempotencyKey string } type ClientExecutionEvent struct { ID string `json:"event_id"` Step string `json:"step"` Type string `json:"type"` Message string `json:"message"` OccurredAt string `json:"occurred_at"` } type AppendExecutionEventsCommand struct { Identity ExecutionResultIdentity Events []ClientExecutionEvent } type UploadExecutionEvidenceCommand struct { Identity ExecutionResultIdentity DeclaredMediaType string Content io.Reader } type ExecutionProvenance struct { ProviderID string `json:"provider_id"` Model string `json:"model"` PromptVersion string `json:"prompt_version"` SchemaVersion int `json:"schema_version"` } type CandidateEvaluation struct { Decision string `json:"decision"` Score float64 `json:"score"` Matched []string `json:"matched"` MissingOrUncertain []string `json:"missing_or_uncertain"` RejectionReasons []string `json:"rejection_reasons"` Confidence float64 `json:"confidence"` HardConstraints []CandidateHardConstraintEvaluation `json:"hard_constraints"` } type CandidateHardConstraintEvaluation struct { Kind string `json:"kind"` Expected string `json:"expected"` Status string `json:"status"` Evidence string `json:"evidence"` } type ExecutionCandidate struct { Ordinal int `json:"ordinal"` Title string `json:"title"` SKUText string `json:"sku_text"` Price string `json:"price"` ProductURL string `json:"product_url"` ImageURL string `json:"image_url"` EvidenceAssetIDs []string `json:"evidence_asset_ids"` Evaluation *CandidateEvaluation `json:"evaluation"` } type CandidateRecommendation struct { CandidateOrdinal int `json:"candidate_ordinal"` PolicyVersion string `json:"policy_version"` Reasons []string `json:"reasons"` } type StoreExecutionCandidatesCommand struct { Identity ExecutionResultIdentity TaskContentSHA256 string ExecutionMode string SearchQuery string Provenance *ExecutionProvenance Candidates []ExecutionCandidate Recommendation *CandidateRecommendation } type CompleteExecutionCommand struct { Identity ExecutionResultIdentity TaskContentSHA256 string ExecutionMode string Outcome string OperatorReason string Candidate *ExecutionCandidate OrderSubmitted bool } type FailExecutionCommand struct { Identity ExecutionResultIdentity ErrorCode string ErrorMessage string ErrorStep string Retryable bool EvidenceAssetIDs []string } type UploadExecutionEvidenceResult struct { Evidence domain.ExecutionEvidenceAsset Replayed bool } type ExecutionEvidenceContent struct { Evidence domain.ExecutionEvidenceAsset Content io.ReadCloser } func NewExecutionResultService( repository ExecutionResultRepository, store ReferenceImageStore, clock Clock, ids IDGenerator, ) (*ExecutionResultService, error) { if repository == nil || store == nil || clock == nil || ids == nil { return nil, errors.New("execution result service dependencies are required") } return &ExecutionResultService{ repository: repository, store: store, clock: clock, ids: ids, }, nil } func (service *ExecutionResultService) AppendEvents( ctx context.Context, command AppendExecutionEventsCommand, ) (bool, error) { identity, err := normalizeExecutionIdentity(command.Identity) if err != nil { return false, err } if len(command.Events) < 1 || len(command.Events) > 32 { return false, executionResultInvalid("events", "must contain 1 to 32 events") } now := service.clock.Now().UTC() events := make([]domain.ExecutionEvent, 0, len(command.Events)) seen := make(map[string]struct{}, len(command.Events)) for _, event := range command.Events { parsed, eventErr := normalizeClientEvent(event, identity.TaskID, identity.ExecutionID, now) if eventErr != nil { return false, eventErr } if _, found := seen[parsed.ID]; found { return false, executionResultInvalid("events", "event_id must be unique") } seen[parsed.ID] = struct{}{} events = append(events, parsed) } requestHash, err := executionResultHash(command.Events) if err != nil { return false, internalExecutionResultFailure(err) } replayed, err := service.repository.AppendExecutionEvents( ctx, service.write(identity, executionResultEventsOperation, requestHash, now), events, ) if err != nil { return false, wrapLifecycleRepositoryError(err) } return replayed, nil } func (service *ExecutionResultService) UploadEvidence( ctx context.Context, command UploadExecutionEvidenceCommand, ) (UploadExecutionEvidenceResult, error) { identity, err := normalizeExecutionIdentity(command.Identity) if err != nil { return UploadExecutionEvidenceResult{}, err } if command.Content == nil { return UploadExecutionEvidenceResult{}, executionResultInvalid("file", "required") } evidenceID, err := service.ids.NewID() if err != nil { return UploadExecutionEvidenceResult{}, internalExecutionResultFailure(err) } normalized, err := service.store.Put( ctx, evidenceID, command.DeclaredMediaType, command.Content, ) if err != nil { return UploadExecutionEvidenceResult{}, mapImageStoreError(err) } cleanup := func() { _ = service.store.Delete(context.Background(), normalized.StorageKey) } now := service.clock.Now().UTC() requestHash, err := executionResultHash(struct { InputSHA256 string `json:"input_sha256"` }{InputSHA256: normalized.InputSHA256}) if err != nil { cleanup() return UploadExecutionEvidenceResult{}, internalExecutionResultFailure(err) } evidence, replayed, err := service.repository.CreateExecutionEvidence( ctx, service.write(identity, executionResultEvidenceOperation, requestHash, now), domain.ExecutionEvidenceAsset{ ID: evidenceID, TaskID: identity.TaskID, ExecutionID: identity.ExecutionID, MediaType: normalized.MediaType, SizeBytes: normalized.SizeBytes, SHA256: normalized.SHA256, StorageKey: normalized.StorageKey, CreatedAt: now, }, ) if err != nil { cleanup() return UploadExecutionEvidenceResult{}, wrapLifecycleRepositoryError(err) } if replayed { cleanup() } return UploadExecutionEvidenceResult{Evidence: evidence, Replayed: replayed}, nil } func (service *ExecutionResultService) StoreCandidates( ctx context.Context, command StoreExecutionCandidatesCommand, ) (bool, error) { identity, err := normalizeExecutionIdentity(command.Identity) if err != nil { return false, err } if err := validateCandidateCommand(command); err != nil { return false, err } provenanceJSON, candidatesJSON, recommendationJSON, err := candidateJSON(command) if err != nil { return false, internalExecutionResultFailure(err) } now := service.clock.Now().UTC() requestHash, err := executionResultHash(command) if err != nil { return false, internalExecutionResultFailure(err) } replayed, err := service.repository.StoreExecutionCandidates( ctx, service.write(identity, executionResultCandidatesOperation, requestHash, now), domain.ExecutionCandidateBatch{ TaskID: identity.TaskID, ExecutionID: identity.ExecutionID, TaskContentSHA256: command.TaskContentSHA256, ExecutionMode: command.ExecutionMode, SearchQuery: strings.TrimSpace(command.SearchQuery), ProvenanceJSON: provenanceJSON, CandidatesJSON: candidatesJSON, RecommendationJSON: recommendationJSON, ReceivedAt: now, }, ) if err != nil { return false, wrapLifecycleRepositoryError(err) } return replayed, nil } func (service *ExecutionResultService) Complete( ctx context.Context, command CompleteExecutionCommand, ) (domain.PurchaseTask, bool, error) { identity, err := normalizeExecutionIdentity(command.Identity) if err != nil { return domain.PurchaseTask{}, false, err } if err := validateCompleteCommand(command); err != nil { return domain.PurchaseTask{}, false, err } selectedJSON, err := optionalJSON(command.Candidate) if err != nil { return domain.PurchaseTask{}, false, internalExecutionResultFailure(err) } now := service.clock.Now().UTC() requestHash, err := executionResultHash(command) if err != nil { return domain.PurchaseTask{}, false, internalExecutionResultFailure(err) } mode := command.ExecutionMode taskHash := command.TaskContentSHA256 outcome := command.Outcome reason := strings.TrimSpace(command.OperatorReason) task, replayed, err := service.repository.CompleteExecution( ctx, service.write(identity, executionResultCompleteOperation, requestHash, now), domain.ExecutionOutcome{ TaskID: identity.TaskID, ExecutionID: identity.ExecutionID, ResultType: "COMPLETE", ExecutionMode: &mode, TaskContentSHA256: &taskHash, Outcome: &outcome, OperatorReason: &reason, SelectedCandidateJSON: selectedJSON, OrderSubmitted: false, ReceivedAt: now, }, ) if err != nil { return domain.PurchaseTask{}, false, wrapLifecycleRepositoryError(err) } return task, replayed, nil } func (service *ExecutionResultService) Fail( ctx context.Context, command FailExecutionCommand, ) (domain.PurchaseTask, bool, error) { identity, err := normalizeExecutionIdentity(command.Identity) if err != nil { return domain.PurchaseTask{}, false, err } if err := validateFailCommand(command); err != nil { return domain.PurchaseTask{}, false, err } evidenceJSON, err := optionalJSON(command.EvidenceAssetIDs) if err != nil { return domain.PurchaseTask{}, false, internalExecutionResultFailure(err) } now := service.clock.Now().UTC() requestHash, err := executionResultHash(command) if err != nil { return domain.PurchaseTask{}, false, internalExecutionResultFailure(err) } code := strings.TrimSpace(command.ErrorCode) message := strings.TrimSpace(command.ErrorMessage) step := strings.TrimSpace(command.ErrorStep) retryable := command.Retryable task, replayed, err := service.repository.FailExecution( ctx, service.write(identity, executionResultFailOperation, requestHash, now), domain.ExecutionOutcome{ TaskID: identity.TaskID, ExecutionID: identity.ExecutionID, ResultType: "FAIL", EvidenceAssetIDsJSON: evidenceJSON, ErrorCode: &code, ErrorMessage: &message, ErrorStep: &step, Retryable: &retryable, OrderSubmitted: false, ReceivedAt: now, }, ) if err != nil { return domain.PurchaseTask{}, false, wrapLifecycleRepositoryError(err) } return task, replayed, nil } func (service *ExecutionResultService) OpenEvidence( ctx context.Context, taskID string, evidenceID string, ) (ExecutionEvidenceContent, error) { evidence, err := service.repository.GetExecutionEvidence(ctx, taskID, evidenceID) if err != nil { return ExecutionEvidenceContent{}, wrapLifecycleRepositoryError(err) } content, err := service.store.Open(ctx, evidence.StorageKey) if err != nil { return ExecutionEvidenceContent{}, mapImageStoreError(err) } return ExecutionEvidenceContent{Evidence: evidence, Content: content}, nil } func (service *ExecutionResultService) write( identity ExecutionResultIdentity, operation string, requestHash string, now time.Time, ) ExecutionResultWrite { return ExecutionResultWrite{ ExecutionResultAuthorization: ExecutionResultAuthorization{ UserID: identity.UserID, DeviceID: identity.DeviceID, TaskID: identity.TaskID, ExecutionID: identity.ExecutionID, ClaimGeneration: identity.ClaimGeneration, ClaimTokenHash: hashSecret(identity.ClaimToken), }, Operation: operation, IdempotencyKey: identity.IdempotencyKey, RequestHash: requestHash, Now: now, } } func normalizeExecutionIdentity( identity ExecutionResultIdentity, ) (ExecutionResultIdentity, error) { identity.UserID = strings.TrimSpace(identity.UserID) identity.DeviceID = strings.TrimSpace(identity.DeviceID) identity.TaskID = strings.TrimSpace(identity.TaskID) identity.ExecutionID = strings.TrimSpace(identity.ExecutionID) identity.IdempotencyKey = strings.TrimSpace(identity.IdempotencyKey) fields := map[string]string{} if !isUUID(identity.UserID) { fields["user_id"] = "must be a UUID" } if !isUUID(identity.DeviceID) { fields["device_id"] = "must be a UUID" } if !isUUID(identity.TaskID) { fields["task_id"] = "must be a UUID" } if !isUUID(identity.ExecutionID) { fields["execution_id"] = "must be a UUID" } if identity.ClaimGeneration < 1 { fields["claim_generation"] = "must be a positive integer" } if strings.TrimSpace(identity.ClaimToken) == "" { fields["claim_token"] = "required" } if len([]byte(identity.IdempotencyKey)) == 0 || len([]byte(identity.IdempotencyKey)) > maxIdempotencyKeyBytes || !isPrintableASCII(identity.IdempotencyKey) { fields["idempotency_key"] = "must be printable ASCII up to 128 bytes" } if len(fields) > 0 { return ExecutionResultIdentity{}, invalidError( "EXECUTION_RESULT_INVALID", "execution result request is invalid", fields, ) } return identity, nil } func normalizeClientEvent( event ClientExecutionEvent, taskID string, executionID string, now time.Time, ) (domain.ExecutionEvent, error) { if !isUUID(strings.TrimSpace(event.ID)) { return domain.ExecutionEvent{}, executionResultInvalid("event_id", "must be a UUID") } step := strings.TrimSpace(event.Step) typeValue := strings.TrimSpace(event.Type) message := strings.TrimSpace(event.Message) if !validLifecycleStep(step) { return domain.ExecutionEvent{}, executionResultInvalid("step", "must be uppercase ASCII") } if !validLifecycleStep(typeValue) { return domain.ExecutionEvent{}, executionResultInvalid("type", "must be uppercase ASCII") } if !validAuditText(message, 1000) { return domain.ExecutionEvent{}, executionResultInvalid("message", "is invalid or contains sensitive material") } occurredAt, err := time.Parse(time.RFC3339Nano, strings.TrimSpace(event.OccurredAt)) if err != nil || occurredAt.After(now.Add(5*time.Minute)) || occurredAt.Before(now.Add(-7*24*time.Hour)) { return domain.ExecutionEvent{}, executionResultInvalid("occurred_at", "must be a recent RFC3339 timestamp") } return domain.ExecutionEvent{ ID: strings.TrimSpace(event.ID), TaskID: taskID, ExecutionID: executionID, Step: step, Type: typeValue, Message: message, OccurredAt: occurredAt.UTC(), ReceivedAt: now, }, nil } func validateCandidateCommand(command StoreExecutionCandidatesCommand) error { if !sha256Pattern.MatchString(command.TaskContentSHA256) { return executionResultInvalid("task_content_sha256", "must be lowercase SHA-256") } if !validExecutionMode(command.ExecutionMode) { return executionResultInvalid("execution_mode", "must be MANUAL_FIRST or AI_ASSISTED") } if !validAuditText(command.SearchQuery, 512) { return executionResultInvalid("search_query", "is invalid") } if len(command.Candidates) > 5 { return executionResultInvalid("candidates", "must contain at most 5 candidates") } if command.ExecutionMode == aiAssistedMode { if !validProvenance(command.Provenance) { return executionResultInvalid("provenance", "is required for AI_ASSISTED") } } else if command.Provenance != nil { return executionResultInvalid("provenance", "must be omitted for MANUAL_FIRST") } for index, candidate := range command.Candidates { if candidate.Ordinal != index+1 || !validCandidate(candidate, command.ExecutionMode) { return executionResultInvalid("candidates", "must be continuous, bounded observations") } } if command.ExecutionMode == manualFirstMode && command.Recommendation != nil { return executionResultInvalid( "recommendation", "must be omitted for MANUAL_FIRST", ) } if command.Recommendation != nil { recommendation := command.Recommendation if recommendation.CandidateOrdinal < 1 || recommendation.CandidateOrdinal > len(command.Candidates) || !validAuditText(recommendation.PolicyVersion, 128) || !validStringList(recommendation.Reasons, 8, 160) || (command.ExecutionMode == aiAssistedMode && command.Provenance.SchemaVersion >= 2 && !validSKUMatchedCandidate( command.Candidates[recommendation.CandidateOrdinal-1], )) { return executionResultInvalid("recommendation", "is invalid") } } return nil } func validateCompleteCommand(command CompleteExecutionCommand) error { if !sha256Pattern.MatchString(command.TaskContentSHA256) || !validExecutionMode(command.ExecutionMode) || !validOutcome(command.Outcome) || !validAuditText(command.OperatorReason, 1000) || command.OrderSubmitted { return executionResultInvalid("complete", "contains an invalid outcome or order state") } if command.Outcome == "CANDIDATE_ACCEPTED" { if command.Candidate == nil || !validCandidate(*command.Candidate, command.ExecutionMode) { return executionResultInvalid("candidate", "is required for CANDIDATE_ACCEPTED") } } else if command.Candidate != nil { return executionResultInvalid("candidate", "must be omitted for this outcome") } return nil } func validateFailCommand(command FailExecutionCommand) error { if !validLifecycleStep(strings.TrimSpace(command.ErrorCode)) || !validLifecycleStep(strings.TrimSpace(command.ErrorStep)) || !validAuditText(command.ErrorMessage, 1000) { return executionResultInvalid("error", "is invalid") } if len(command.EvidenceAssetIDs) > 5 { return executionResultInvalid("evidence_asset_ids", "must contain at most 5 items") } for _, id := range command.EvidenceAssetIDs { if !isUUID(strings.TrimSpace(id)) { return executionResultInvalid("evidence_asset_ids", "must contain UUIDs") } } return nil } func validCandidate(candidate ExecutionCandidate, mode string) bool { if candidate.Ordinal < 1 || !validAuditText(candidate.Title, 512) || !validOptionalAuditText(candidate.SKUText, 512) || !validOptionalAuditText(candidate.Price, 64) || !validObservationURL(candidate.ProductURL) || !validObservationURL(candidate.ImageURL) || len(candidate.EvidenceAssetIDs) > 5 { return false } for _, id := range candidate.EvidenceAssetIDs { if !isUUID(strings.TrimSpace(id)) { return false } } if mode == manualFirstMode { return candidate.Evaluation == nil } return validEvaluation(candidate.Evaluation) } func validEvaluation(value *CandidateEvaluation) bool { if value == nil || value.Score < 0 || value.Score > 1 || value.Confidence < 0 || value.Confidence > 1 { return false } if value.Decision != "REVIEW" && value.Decision != "REJECT" && value.Decision != "MANUAL_REQUIRED" { return false } return validStringList(value.Matched, 12, 160) && validStringList(value.MissingOrUncertain, 12, 160) && validStringList(value.RejectionReasons, 12, 160) && validCandidateHardConstraints(value.HardConstraints) } func validSKUMatchedCandidate(candidate ExecutionCandidate) bool { value := candidate.Evaluation return value != nil && value.Decision == "REVIEW" && value.Score >= 0.75 && value.Confidence >= 0.75 && len(value.RejectionReasons) == 0 && len(value.HardConstraints) == 2 && allHardConstraintsMatch(value.HardConstraints) } func validCandidateHardConstraints( values []CandidateHardConstraintEvaluation, ) bool { if len(values) == 0 { return true } if len(values) != 2 { return false } seen := map[string]struct{}{} for _, value := range values { if (value.Kind != "COLOR" && value.Kind != "SIZE") || (value.Status != "MATCH" && value.Status != "MISMATCH" && value.Status != "UNKNOWN") || !validAuditText(value.Expected, 128) || !validAuditText(value.Evidence, 160) { return false } if _, duplicate := seen[value.Kind]; duplicate { return false } seen[value.Kind] = struct{}{} } return len(seen) == 2 } func allHardConstraintsMatch( values []CandidateHardConstraintEvaluation, ) bool { for _, value := range values { if value.Status != "MATCH" { return false } } return true } func validProvenance(value *ExecutionProvenance) bool { return value != nil && validAuditText(value.ProviderID, 64) && validAuditText(value.Model, 256) && validAuditText(value.PromptVersion, 128) && value.SchemaVersion >= 1 && value.SchemaVersion <= 32 } func validExecutionMode(value string) bool { return value == manualFirstMode || value == aiAssistedMode } func validOutcome(value string) bool { switch value { case "CANDIDATE_ACCEPTED", "CANDIDATE_REJECTED", "NO_MATCH", "MANUAL_REQUIRED": return true default: return false } } func validObservationURL(value string) bool { if strings.TrimSpace(value) == "" { return true } parsed, err := url.Parse(strings.TrimSpace(value)) return err == nil && (parsed.Scheme == "https" || parsed.Scheme == "http") && parsed.Host != "" && parsed.User == nil && len(value) <= 2048 } func validAuditText(value string, maximum int) bool { value = strings.TrimSpace(value) if value == "" || !utf8.ValidString(value) || len([]byte(value)) > maximum { return false } lower := strings.ToLower(value) return !strings.Contains(lower, "authorization:") && !strings.Contains(lower, "api_key") && !strings.Contains(lower, "bearer ") } func validOptionalAuditText(value string, maximum int) bool { return strings.TrimSpace(value) == "" || validAuditText(value, maximum) } func validStringList(values []string, maximumItems int, maximumText int) bool { if len(values) > maximumItems { return false } seen := map[string]struct{}{} for _, value := range values { if !validAuditText(value, maximumText) { return false } key := strings.ToLower(strings.TrimSpace(value)) if _, duplicate := seen[key]; duplicate { return false } seen[key] = struct{}{} } return true } func candidateJSON(command StoreExecutionCandidatesCommand) (*string, string, *string, error) { var provenance *string if command.Provenance != nil { encoded, err := json.Marshal(command.Provenance) if err != nil { return nil, "", nil, err } value := string(encoded) provenance = &value } candidates, err := json.Marshal(command.Candidates) if err != nil { return nil, "", nil, err } var recommendation *string if command.Recommendation != nil { encoded, err := json.Marshal(command.Recommendation) if err != nil { return nil, "", nil, err } value := string(encoded) recommendation = &value } return provenance, string(candidates), recommendation, nil } func optionalJSON(value any) (*string, error) { if value == nil { return nil, nil } encoded, err := json.Marshal(value) if err != nil { return nil, err } result := string(encoded) return &result, nil } func executionResultHash(value any) (string, error) { encoded, err := json.Marshal(value) if err != nil { return "", err } sum := sha256.Sum256(encoded) return hex.EncodeToString(sum[:]), nil } func TaskContentSHA256(task domain.PurchaseTask) string { budget := "" if task.MaxBudgetCents != nil { budget = strconv.FormatInt(*task.MaxBudgetCents, 10) } payload := strings.Join([]string{ task.Title, task.Description, task.SKU, task.ImageAssetID, strconv.Itoa(task.Quantity), budget, task.Currency, }, "\x00") sum := sha256.Sum256([]byte(payload)) return hex.EncodeToString(sum[:]) } func executionResultInvalid(field string, message string) error { return invalidError( "EXECUTION_RESULT_INVALID", "execution result request is invalid", map[string]string{field: message}, ) } func internalExecutionResultFailure(err error) error { return newError( ErrorKindInternal, "INTERNAL_ERROR", "internal server error", err, ) } var sha256Pattern = regexp.MustCompile("^[0-9a-f]{64}$")