obsidian/wiki/concepts/workflow-approve-dispatch-completeness.md
2026-05-10 21:14:23 +01:00

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