-- allium: 1 -- bDS Media Lifecycle -- Scope: core (Wave 1) -- Distilled from: src/main/engine/MediaEngine.ts, schema.ts use "./project.allium" as project surface MediaControlSurface { facing _: MediaOperator provides: ImportMediaRequested(project, source_file) UpdateMediaRequested(media, changes) DeleteMediaRequested(media) UpsertMediaTranslationRequested(media, language, title, alt, caption) RebuildMediaFromFilesRequested(project) } value ThumbnailSet { small: String -- 150px width (binary path) medium: String -- 400px width (binary path) large: String -- 800px width (binary path) ai: String -- 448x448 JPEG for vision models (binary path) } value SidecarFile { -- {media_file}.meta (YAML-like key-value format) -- Fields: title, alt, caption, author, tags, language, linkedPostIds -- Translations: {media_file}.{lang}.meta path: String } surface SidecarFileSurface { context sidecar: SidecarFile exposes: sidecar.path } entity Media { project: project/Project filename: String original_name: String mime_type: String size: Integer width: Integer? height: Integer? title: String? alt: String? caption: String? author: String? language: String? file_path: String sidecar_path: String checksum: String? tags: List created_at: Timestamp updated_at: Timestamp -- Relationships translations: MediaTranslation with media = this linked_posts: PostMediaLink with media_id = this.id -- Derived available_languages: translations -> language thumbnails: ThumbnailSet } surface MediaSurface { context media: Media exposes: media.project media.filename media.original_name media.mime_type media.size media.width when media.width != null media.height when media.height != null media.title when media.title != null media.alt when media.alt != null media.caption when media.caption != null media.author when media.author != null media.language when media.language != null media.file_path media.sidecar_path media.checksum when media.checksum != null media.tags media.created_at media.updated_at media.translations.count media.linked_posts.count media.available_languages media.thumbnails.small media.thumbnails.medium media.thumbnails.large media.thumbnails.ai } entity MediaTranslation { media: Media language: String title: String? alt: String? caption: String? } invariant UniqueMediaTranslation { for a in MediaTranslations: for b in MediaTranslations: (a != b and a.media = b.media) implies a.language != b.language } invariant DateBasedMediaLayout { for m in Media: m.file_path = format("media/{yyyy}/{mm}/{uuid}.{ext}", yyyy: m.created_at.year, mm: m.created_at.month_padded, uuid: stem(m.filename), ext: extension(m.filename)) } rule ImportMedia { when: ImportMediaRequested(project, source_file) let uuid_name = generate_uuid() + extension(source_file) let dest = format("media/{yyyy}/{mm}/{uuid_name}", yyyy: now.year, mm: now.month_padded) ensures: Media.created( project: project, filename: uuid_name, original_name: source_file.name, mime_type: detect_mime(source_file), size: source_file.size, width: detect_width(source_file), height: detect_height(source_file), file_path: dest, tags: {} ) ensures: FileCopied(source_file, dest) ensures: SidecarWritten(media) ensures: ThumbnailsGenerated(media) ensures: SearchIndexUpdated(media) } rule UpdateMedia { when: UpdateMediaRequested(media, changes) ensures: MediaFieldsUpdated(media, changes) ensures: media.updated_at = now ensures: SidecarWritten(media) -- Metadata changes flush to .meta sidecar ensures: SearchIndexUpdated(media) } rule DeleteMedia { when: DeleteMediaRequested(media) ensures: not exists media ensures: MediaFileDeleted(media) ensures: SidecarDeleted(media) ensures: ThumbnailsDeleted(media) ensures: for t in media.translations: not exists t ensures: SearchIndexUpdated(media) } rule UpsertMediaTranslation { when: UpsertMediaTranslationRequested(media, language, title, alt, caption) ensures: MediaTranslation.created( media: media, language: language, title: title, alt: alt, caption: caption ) ensures: TranslationSidecarWritten(media, language) -- Writes {file}.{lang}.meta } rule RebuildMediaFromFiles { when: RebuildMediaFromFilesRequested(project) -- Scans media directory for .meta sidecars, reimports to DB for sidecar in scan_directory(project.effective_data_dir + "/media", "*.meta"): let parsed = parse_sidecar(sidecar) ensures: Media.created(parsed) -- or updated if already exists @guidance -- This is the filesystem-to-DB reconciliation path -- Used after git pull or manual file changes } invariant SidecarRoundtrip { -- Sidecar files faithfully represent DB metadata for m in Media: parse_sidecar(m.sidecar_path).title = m.title parse_sidecar(m.sidecar_path).alt = m.alt parse_sidecar(m.sidecar_path).caption = m.caption parse_sidecar(m.sidecar_path).tags = m.tags }