@@ -138,6 +138,13 @@ class UnsafeCandidateError extends Error {
138138 }
139139}
140140
141+ class DeferredCandidateError extends Error {
142+ constructor ( readonly code : 'field_mapping_missing' , message : string ) {
143+ super ( message )
144+ this . name = 'DeferredCandidateError'
145+ }
146+ }
147+
141148function unsafe ( code : string , issue : string | string [ ] ) : never {
142149 throw new UnsafeCandidateError ( code , Array . isArray ( issue ) ? issue : [ issue ] )
143150}
@@ -503,7 +510,12 @@ async function buildPlan(
503510 const facts : PlannedFact [ ] = [ ]
504511 for ( const fact of candidate . facts ) {
505512 const mapping = byField . get ( fact . fieldPath )
506- if ( ! mapping ) unsafe ( 'field_mapping_missing' , `No exact promotion mapping for ${ fact . fieldPath } ` )
513+ if ( ! mapping ) {
514+ throw new DeferredCandidateError (
515+ 'field_mapping_missing' ,
516+ `No exact promotion mapping for ${ fact . fieldPath } ` ,
517+ )
518+ }
507519 if ( [ 'quarantined' , 'archived' , 'rejected' ] . includes ( mapping . workflow_status ) ) {
508520 unsafe ( 'target_record_blocked' , `Target record ${ mapping . subject_record_id } is ${ mapping . workflow_status } ` )
509521 }
@@ -1063,6 +1075,11 @@ export async function promoteCandidate(
10631075 const candidate = await validateCandidate ( row )
10641076 plan = await buildPlan ( database , candidate )
10651077 } catch ( error ) {
1078+ if ( error instanceof DeferredCandidateError ) {
1079+ // Mapping is an operator-owned dependency, not an evidence failure.
1080+ // Keep the validated candidate untouched so a later poll can retry it.
1081+ return { candidateId, status : 'deferred' , reasonCode : error . code }
1082+ }
10661083 if ( ! ( error instanceof UnsafeCandidateError ) ) throw error
10671084 await isolateCandidate ( database , candidateId , error . code , error . issues , now )
10681085 return { candidateId, status : 'quarantined' , reasonCode : error . code }
0 commit comments