Skip to content

Commit 58470c8

Browse files
Merge pull request #27 from computersciencefreshmen/codex/publisher-mapping-recovery
Keep validated candidates recoverable until exact mappings exist
2 parents 47c30a4 + 155a0c5 commit 58470c8

4 files changed

Lines changed: 334 additions & 12 deletions

File tree

workers/publisher/src/index.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,24 @@ async function scheduleValidatedCandidates(
4444
ON promotion.candidate_id = candidate.candidate_id
4545
WHERE candidate.candidate_status = 'validated'
4646
AND candidate.gate_status IN ('rule-pass', 'dual-pass')
47+
AND json_array_length(candidate.facts_json) > 0
48+
AND NOT EXISTS (
49+
SELECT 1
50+
FROM json_each(candidate.facts_json) fact
51+
WHERE NOT EXISTS (
52+
SELECT 1
53+
FROM promotion_field_mappings mapping
54+
JOIN records target ON target.id = mapping.subject_record_id
55+
JOIN field_definitions definition
56+
ON definition.record_kind = target.kind
57+
AND definition.field_path = mapping.canonical_field_path
58+
WHERE mapping.source_id = candidate.source_id
59+
AND mapping.enabled = 1
60+
AND mapping.candidate_field_path = CASE
61+
WHEN fact.type = 'object' THEN json_extract(fact.value, '$.fieldPath')
62+
END
63+
)
64+
)
4765
AND (
4866
promotion.candidate_id IS NULL
4967
OR (
@@ -94,6 +112,8 @@ async function handleQueue(
94112
if (result.status === 'busy') {
95113
message.retry({ delaySeconds: 60 })
96114
} else {
115+
// A deferred dependency cannot be repaired by queue retries. The
116+
// scheduler will rediscover it once every exact mapping is available.
97117
message.ack()
98118
}
99119
} catch (error) {

workers/publisher/src/promoter.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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+
141148
function 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 }

workers/publisher/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export interface PublisherEnv {
6565

6666
export type PromotionResult = {
6767
candidateId: string
68-
status: 'applied' | 'already-applied' | 'quarantined' | 'busy'
68+
status: 'applied' | 'already-applied' | 'quarantined' | 'busy' | 'deferred'
6969
publicationJobId?: string
7070
reasonCode?: string
7171
}

0 commit comments

Comments
 (0)