-- allium: 1 -- bDS Metadata Diff and Rebuild -- Scope: core (Wave 1) -- Distilled from: src/main/engine/MetadataDiffEngine.ts use "./post.allium" as post use "./media.allium" as media use "./script.allium" as script use "./template.allium" as template surface MetadataMaintenanceSurface { facing _: MaintenanceOperator provides: MetadataDiffRequested(project) RebuildDatabaseRequested(project) RebuildFromFilesystemRequested(project, entity_type) RepairMetadataDiffItemRequested(project, direction, item) } value DiffField { name: String db_value: String file_value: String } value DiffReport { entity_type: String -- project, categories, category_meta, publishing, -- post, post_translation, media, media_translation, -- script, template, embedding entity_id: String differences: List } value OrphanReport { file_path: String -- File exists on disk but has no DB record } rule RunMetadataDiff { when: MetadataDiffRequested(project) -- Runs as background task via TaskManager -- Compares DB records against filesystem files for: -- project metadata (including a missing description), posts, -- translations, media, Lua scripts, templates -- Detected fields: tags, categories, title, excerpt, author, -- language, status, templateSlug, dates -- Portable project metadata has a project-scoped database snapshot in the -- settings table. Compare its project, category list, per-category settings, -- and publishing preferences against the matching meta/*.json files. for metadata_type in {"project", "categories", "category_meta", "publishing"}: let db_metadata = ProjectMetadataSetting(project.id, metadata_type) let file_metadata = ReadProjectMetadataFile(project, metadata_type) let metadata_diffs = compare_fields(db_metadata, file_metadata) if metadata_diffs.count > 0: ensures: DiffReport.created( entity_type: metadata_type, entity_id: project.id, differences: metadata_diffs ) for post in project.posts: let file_data = parse_post_file(post.file_path) let diffs = compare_fields(post, file_data) if diffs.count > 0: ensures: DiffReport.created(entity_type: "post", entity_id: post.id, differences: diffs) -- Translation files only carry language-specific metadata. Shared status and -- timestamp fields come from the canonical post and must not be reported as -- missing when they are absent from the translation file. for translation in project.post_translations: let translation_file_data = parse_post_file(translation.file_path) let translation_diffs = compare_translation_specific_fields(translation, translation_file_data) if translation_diffs.count > 0: ensures: DiffReport.created( entity_type: "post_translation", entity_id: translation.id, differences: translation_diffs ) -- Detect orphan files (on disk but not in DB) for file in scan_directory(project.public_dir + "/posts", "*.md"): let matching = Posts where file_path = file if matching.count = 0: ensures: OrphanReport.created(file_path: file) -- Same pattern for media sidecars (media), media translation sidecars -- (media_translation), scripts, templates, and embeddings. -- Embedding diffs compare the stored content_hash against the live post -- content to detect vectors that need recomputation. } rule RebuildDatabase { when: RebuildDatabaseRequested(project) -- The filesystem is the complete source of truth for portable project data. -- Existing project-scoped content and relationship rows are removed before -- project.json, posts, translations, media sidecars and links, tags, -- templates, and Lua scripts are imported. Python files in scripts/ are ignored. ensures: ProjectMetadataImported(project, "meta/project.json") ensures: ProjectMetadataSettingsImported(project, "meta/*.json") ensures: post/RebuildPostsFromFiles(project) ensures: media/RebuildMediaFromFiles(project) ensures: script/RebuildScriptsFromFiles(project) ensures: template/RebuildTemplatesFromFiles(project) @guidance -- Rebuilding media trusts persisted sidecar size, dimensions, and metadata. -- Thumbnail regeneration remains a separate maintenance operation. } rule RepairMetadataDiffItem { when: RepairMetadataDiffItemRequested(project, direction, item) -- Resolves a single diff in one direction. -- direction = file_to_db (filesystem wins) | db_to_file (database wins) -- Dispatched per item.entity_type: -- project | categories | category_meta | publishing -> project metadata sync/flush -- post -> sync_post_from_file / rewrite_published_post -- post_translation -> sync_post_translation_from_file / rewrite_published_post_translation -- media -> sync_media_from_sidecar / sync_media_sidecar -- media_translation -> sync_media_translation_from_sidecar / sync_media_translation_sidecar -- script -> sync_script_from_file / sync_published_script_file -- template -> sync_template_from_file / sync_published_template_file -- embedding -> sync_post (file_to_db) / refresh_snapshot (db_to_file) -- Unknown entity_type or direction -> unsupported error. } rule RebuildFromFilesystem { when: RebuildFromFilesystemRequested(project, entity_type) -- The inverse of metadata diff: filesystem is treated as truth -- Reads all files and upserts into DB ensures: if entity_type = "post": post/RebuildPostsFromFiles(project) if entity_type = "media": media/RebuildMediaFromFiles(project) if entity_type = "script": script/RebuildScriptsFromFiles(project) if entity_type = "template": template/RebuildTemplatesFromFiles(project) } invariant ThreeWaySync { -- Metadata must stay in sync across three representations: -- 1. Database records -- 2. Filesystem files (frontmatter/sidecars) -- 3. Generated site output -- MetadataDiff detects divergence between (1) and (2) -- Rebuild resolves divergence by treating (2) as truth -- Site generation consumes (1) to produce (3) }