.tasks/core/VSS-002-job-system-integration.md
Implement automatic sidecar generation by integrating with the job system. This completes the "generate once, use everywhere" workflow for derivative data.
Currently, SidecarManager::enqueue_generation() creates pending records but never dispatches actual generation jobs (TODO at core/src/service/sidecar_manager.rs:273).
See workbench/core/storage/VIRTUAL_SIDECAR_SYSTEM_V2.md Section "Generation Pipeline" for complete specification.
core/src/service/sidecar_manager.rs - Remove TODO, dispatch jobscore/src/ops/media/thumbnail/job.rs - Already exists, needs sidecar integrationcore/src/ops/media/ocr/job.rs - New job for OCR extractioncore/src/ops/media/transcript/job.rs - New job for transcriptioncore/src/ops/indexing/phases/intelligence_queueing.rs - New indexing phaseThumbnailJob with SidecarManagerOcrExtractionJob structTranscriptGenerationJob structSidecarManager::enqueue_generation()All generation jobs must follow this contract:
#[derive(Job)]
pub struct SidecarGenerationJob {
pub content_uuid: Uuid,
pub kind: SidecarKind,
pub variant: SidecarVariant,
pub config: JobConfig,
}
impl SidecarGenerationJob {
async fn execute(&self, ctx: JobContext) -> Result<JobOutput> {
// 1. Fast-path: check if sidecar already exists
let path = ctx.sidecar_manager.compute_path(...)?;
if fs::exists(&path.absolute_path).await? {
return Ok(JobOutput::AlreadyExists);
}
// 2. Find source file for this content
let source = ctx.find_entry_by_content_uuid(&self.content_uuid).await?;
// 3. Generate sidecar
let sidecar_data = self.generate(source).await?;
// 4. Write to deterministic path
fs::create_dir_all(path.parent()).await?;
fs::write(&path.absolute_path, sidecar_data).await?;
// 5. Record in database
ctx.sidecar_manager.record_sidecar(...).await?;
Ok(JobOutput::Generated { size })
}
}
impl IndexingPipeline {
async fn run_phases(&self, entry: Entry) -> Result<()> {
// Existing phases
self.discovery_phase(entry).await?;
self.processing_phase(entry).await?;
self.aggregation_phase(entry).await?;
self.content_identification_phase(entry).await?;
// NEW: Intelligence queueing phase
if let Some(content_uuid) = entry.content_uuid {
self.intelligence_queueing_phase(content_uuid, &entry.file_type).await?;
}
Ok(())
}
async fn intelligence_queueing_phase(
&self,
content_uuid: Uuid,
file_type: &FileType,
) -> Result<()> {
let specs = self.compute_sidecar_specs(file_type);
for spec in specs {
self.sidecar_manager.enqueue_generation(
&self.library,
&content_uuid,
&spec.kind,
&spec.variant,
&spec.format,
).await?;
}
Ok(())
}
}
pub struct DeviceGenerationPolicy {
pub device_type: DeviceType,
pub enabled_kinds: HashSet<SidecarKind>,
pub variants: HashMap<SidecarKind, Vec<SidecarVariant>>,
}
impl DeviceGenerationPolicy {
pub fn for_mobile() -> Self {
Self {
device_type: DeviceType::Mobile,
enabled_kinds: hashset![SidecarKind::Thumb],
variants: hashmap! {
SidecarKind::Thumb => vec!["grid@2x", "icon"],
},
}
}
pub fn for_desktop() -> Self {
Self {
device_type: DeviceType::Desktop,
enabled_kinds: hashset![
SidecarKind::Thumb,
SidecarKind::Ocr,
SidecarKind::Transcript,
SidecarKind::Embeddings,
],
variants: hashmap! {
SidecarKind::Thumb => vec!["grid@2x", "detail@1x", "grid@3x"],
SidecarKind::Ocr => vec!["default"],
SidecarKind::Transcript => vec!["default"],
SidecarKind::Embeddings => vec!["all-MiniLM-L6-v2"],
},
}
}
}
Estimated: 1 week focused work
Requires VSS-001 (SdPath integration) to be complete for full testing, but can be developed in parallel.