fix: reconcile physique targeting for queued posts
This commit is contained in:
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
|
||||
|
||||
Reference in new issue
Block a user