fix: reconcile physique targeting for queued posts

This commit is contained in:
jjbrunton committed 2026-08-10 18:23:23 +01:00
1 parent d0a8bd0e2f
commit d8d635e1ee
10 files changed
+250 -12

No files matched your search

@@ -1,5 +1,7 @@
import type Database from 'better-sqlite3';
import type { PhysiqueTargeting } from '../config/index.js';
import type { RedditPost } from '../reddit/api.js';
import { evaluatePhysiqueTargeting } from '../services/physique-targeting.js';
import type { ProcessedPost } from '../services/processor.js';
export type CandidateStatus =
@@ -47,6 +49,11 @@ export interface CandidateEnqueueResult {
deferredAtCapacity: number;
}
export interface PhysiqueTargetingReconciliation {
rejected: number;
retained: number;
}
const ACTIVE_STATUSES: CandidateStatus[] = ['discovered', 'screening', 'ready', 'sending', 'retryable'];
/** Global, durable funnel from Reddit listing discovery through outbound delivery. */
@@ -243,6 +250,54 @@ export class CandidateQueueRepository {
`).run(now, now + sendingDelayMs, now).changes;
}
/** Re-evaluates prepared image candidates that have not yet been leased for sending. */
reconcilePhysiqueTargeting(
targeting: PhysiqueTargeting,
now: number = Date.now(),
): PhysiqueTargetingReconciliation {
const rows = this.db.prepare(`
SELECT post_id, prepared_json FROM candidate_queue
WHERE source_type = 'image'
AND status IN ('ready', 'retryable')
AND prepared_json IS NOT NULL
`).all() as Array<{ post_id: string; prepared_json: string }>;
const preparedRows = rows.map((row) => ({
postId: row.post_id,
prepared: JSON.parse(row.prepared_json) as ProcessedPost,
}));
const reject = this.db.prepare(`
UPDATE candidate_queue
SET status = 'rejected', last_error = ?, updated_at = ?,
lease_owner = NULL, lease_expires_at = NULL, assigned_account_id = NULL
WHERE post_id = ? AND status IN ('ready', 'retryable')
`);
const retain = this.db.prepare(`
UPDATE candidate_queue
SET prepared_json = ?, updated_at = ?
WHERE post_id = ? AND status IN ('ready', 'retryable')
`);
return this.db.transaction(() => {
const result: PhysiqueTargetingReconciliation = { rejected: 0, retained: 0 };
for (const row of preparedRows) {
const score = typeof row.prepared.physiqueScore === 'number'
? row.prepared.physiqueScore
: null;
const decision = evaluatePhysiqueTargeting(score, targeting);
if (!decision.accepted) {
result.rejected += reject.run(decision.reason, now, row.postId).changes;
continue;
}
const refreshed: ProcessedPost = {
...row.prepared,
physiqueTargeting: { ...targeting },
};
result.retained += retain.run(JSON.stringify(refreshed), now, row.postId).changes;
}
return result;
})();
}
expireStale(cutoff: number, now: number = Date.now()): number {
return this.db.prepare(`
UPDATE candidate_queue