105 lines
4.5 KiB
Markdown
105 lines
4.5 KiB
Markdown
---
|
|
title: "Workflow Approve: Completeness-Check Dispatch Pattern"
|
|
aliases: [approve-source-pattern, workflow-auto-dispatch, pipeline-approve-transition]
|
|
tags: [workflow, pipeline, fastapi, python, state-machine, typescript]
|
|
sources:
|
|
- "daily/2026-05-06.md"
|
|
created: 2026-05-06
|
|
updated: 2026-05-06
|
|
---
|
|
|
|
# Workflow Approve: Completeness-Check Dispatch Pattern
|
|
|
|
When an approval endpoint sits at the boundary of a multi-step pipeline (e.g., "approve source language" before machine translation begins), the handler should check whether all downstream outputs already exist before dispatching new work. If they do — advance state directly to the final review stage. If they don't — dispatch processing and set state to `TRANSLATING` (or equivalent in-progress state).
|
|
|
|
## Key Takeaways
|
|
|
|
- An `approve_source` endpoint that blindly advances state without dispatching translation leaves the pipeline permanently stalled — translation tasks never start
|
|
- The completeness check and dispatch belong **in the handler**, not in a separate Celery signal or post-save hook — the condition (all languages translated?) is already available at the handler's scope
|
|
- Separate the "single language re-trigger" case into its own endpoint (`retranslate-language`) with different validation: EN must be already approved, target lang must not equal source
|
|
- TypeScript literal union removal is compile-time-only: removing `'video_native'` from `TranslationMode` causes silent failures in all hardcoded usages — grep for the string first
|
|
|
|
## Details
|
|
|
|
### The Broken Pattern
|
|
|
|
```python
|
|
# ❌ BAD — approve_source just flips status, never dispatches translation
|
|
@router.post("/{job_id}/approve_source")
|
|
async def approve_source(job_id: str, db: AsyncSession = Depends(get_db)):
|
|
job = await get_job(db, job_id)
|
|
job.status = JobStatus.PENDING_FINAL_REVIEW # ← skips dispatch entirely
|
|
await db.commit()
|
|
return {"status": job.status}
|
|
```
|
|
|
|
Every role can call this and translation silently never starts.
|
|
|
|
### The Fixed Pattern
|
|
|
|
```python
|
|
# ✅ GOOD — completeness check decides whether to dispatch or advance
|
|
@router.post("/{job_id}/approve_source")
|
|
async def approve_source(job_id: str, db: AsyncSession = Depends(get_db)):
|
|
job = await get_job(db, job_id)
|
|
|
|
# Check if all target languages already have translations
|
|
missing_languages = [
|
|
lang for lang in job.target_languages
|
|
if not await translation_exists(db, job_id, lang)
|
|
]
|
|
|
|
if missing_languages:
|
|
# Some languages still need processing — dispatch and wait
|
|
job.status = JobStatus.TRANSLATING
|
|
await db.commit()
|
|
dispatch_translation_tasks(job_id, missing_languages)
|
|
else:
|
|
# All translations already present — skip to review
|
|
job.status = JobStatus.PENDING_FINAL_REVIEW
|
|
await db.commit()
|
|
|
|
return {"status": job.status}
|
|
```
|
|
|
|
### Separate Endpoint for Single-Language Re-trigger
|
|
|
|
```python
|
|
# Distinct endpoint — different validation rules apply
|
|
@router.post("/{job_id}/retranslate-language")
|
|
async def retranslate_language(
|
|
job_id: str, language: str, db: AsyncSession = Depends(get_db)
|
|
):
|
|
job = await get_job(db, job_id)
|
|
assert job.source_approved, "Source must be approved before re-translating"
|
|
assert language != job.source_language, "Cannot re-translate source language"
|
|
# dispatch single-language task...
|
|
```
|
|
|
|
### TypeScript Literal Union: Safe Removal Checklist
|
|
|
|
When removing a value from a `const` type or literal union:
|
|
|
|
```typescript
|
|
// Before removing 'video_native' from TranslationMode:
|
|
// 1. grep for all usages across the codebase
|
|
// grep -r "video_native" src/
|
|
|
|
// 2. Replace all occurrences with the new value
|
|
// (e.g., 'video_native' → 'traditional')
|
|
|
|
// 3. Build — TypeScript will catch remaining usages
|
|
// npm run build
|
|
```
|
|
|
|
> [!warning] Compile-time-only failure
|
|
> TypeScript literal union mismatches are caught only at build time, not runtime. A value removed from `TranslationMode` will not throw at runtime — it will just pass wrong string values silently until a build reveals the error.
|
|
|
|
## Related Concepts
|
|
|
|
- [[wiki/concepts/celery-queue-worker-specialization]] — task dispatch patterns in Celery
|
|
- [[wiki/concepts/celery-redis-queue-flush-on-deterministic-error]] — when to purge vs retry queued tasks
|
|
|
|
## Sources
|
|
|
|
- [[daily/2026-05-06.md]] — video-accessibility EN-first pipeline: `approve_source` wasn't triggering translation because dispatch was missing from the handler; fixed by adding completeness check and conditional dispatch inside the endpoint
|