-- allium: 1 -- WordPress WXR Import -- Scope: extension (saved analysis, conflict review, recoverable execution) -- Distilled from: ../bDS2/lib/bds/wxr_parser.ex, -- ../bDS2/lib/bds/import_analysis.ex, -- ../bDS2/lib/bds/import_execution.ex, -- ../bDS2/lib/bds/import_definitions.ex, -- ../bDS2/lib/bds/desktop/shell_live/import_editor* use "./project.allium" as project enum ImportItemStatus { new | update | conflict | content_duplicate | missing } enum ImportResolution { ignore | overwrite | import } value ImportedSite { title: String url: String? language: String? } value ImportCandidate { kind: post | page | media source_id: Integer? title: String slug: String? filename: String? status: ImportItemStatus resolution: ImportResolution? existing_id: String? author: String? excerpt: String? content: String? source_status: String? categories: List tags: List source_path: String? parent_source_id: Integer? created_at: Timestamp? updated_at: Timestamp? published_at: Timestamp? } value TaxonomyCandidate { kind: category | tag name: String slug: String? exists_in_project: Boolean mapped_to: String? } value ImportCounts { new_count: Integer update_count: Integer conflict_count: Integer duplicate_count: Integer missing_count: Integer } value ImportDateBucket { year: Integer post_count: Integer media_count: Integer } value ImportMacroUsage { name: String total_count: Integer post_slugs: List } value ImportReport { source_file: String uploads_folder: String? site: ImportedSite posts: List pages: List media: List taxonomies: List post_counts: ImportCounts page_counts: ImportCounts media_counts: ImportCounts date_distribution: List macros: List } entity ImportDefinition { project: project/Project name: String wxr_file_path: String? uploads_folder_path: String? last_analysis: ImportReport? created_at: Timestamp updated_at: Timestamp } config { transaction_batch_size: Integer = 500 } surface ImportDefinitionSurface { context definition: ImportDefinition exposes: definition.project definition.name definition.wxr_file_path when definition.wxr_file_path != null definition.uploads_folder_path when definition.uploads_folder_path != null definition.last_analysis when definition.last_analysis != null definition.created_at definition.updated_at } surface WordPressImportSurface { facing _: ImportOperator exposes: ImportReport ImportCandidate TaxonomyCandidate provides: CreateImportDefinitionRequested(project, name) RenameImportDefinitionRequested(definition, name) SelectWxrFileRequested(definition, file_path) SelectUploadsFolderRequested(definition, folder_path) AnalyzeWxrRequested(definition) ResolveImportConflictRequested(definition, kind, item_name, resolution) MapImportTaxonomyRequested(definition, kind, source_name, target_name) AutoMapImportTaxonomyRequested(definition, model) ExecuteWordPressImportRequested(definition) DeleteImportDefinitionRequested(definition) @guarantee AnalysisReview -- Analysis runs asynchronously and reports localized progress while it -- loads project state, analyzes posts, pages and media, processes -- taxonomy, and discovers WordPress shortcodes. The saved report shows -- item counts, conflicts, missing uploads, year distribution, macro -- usage, and importable totals before any project content is changed. @guarantee ConflictReview -- Conflicts default to ignore. The operator can keep ignoring, overwrite -- the existing item, or import a second item with a unique slug. -- Same-content updates and duplicates are reported but skipped during -- execution, matching bDS2. @guarantee TaxonomyReview -- Existing terms are matched case-insensitively. New source categories -- and tags may be mapped to existing project terms, cleared, or created -- as new terms. Free-text category mappings are normalized before save. @guarantee AiTaxonomyGate -- Optional automatic taxonomy mapping uses the selected configured AI -- model. In airplane mode it runs only through a configured local -- endpoint; otherwise it is refused with localized guidance. The saved -- report is updated only after a successful mapping response. @guarantee ExecutionProgress -- Execution is unavailable when the report contains no importable -- items. While running, the editor shows phase, completed/total count, -- current detail and estimated remaining time for taxonomy, posts, -- media and pages, followed by a localized result summary or error. } rule CreateImportDefinition { when: CreateImportDefinitionRequested(project, name) ensures: ImportDefinition.created( project: project, name: name, wxr_file_path: null, uploads_folder_path: null, last_analysis: null, created_at: now, updated_at: now ) } rule RenameImportDefinition { when: RenameImportDefinitionRequested(definition, name) ensures: definition.name = name ensures: definition.updated_at = now } rule SelectWxrFile { when: SelectWxrFileRequested(definition, file_path) ensures: definition.wxr_file_path = file_path ensures: definition.last_analysis = null ensures: definition.updated_at = now } rule SelectUploadsFolder { when: SelectUploadsFolderRequested(definition, folder_path) ensures: definition.uploads_folder_path = folder_path ensures: definition.updated_at = now } rule AnalyzeWxr { when: AnalyzeWxrRequested(definition) requires: definition.wxr_file_path != null let report = analyze_wordpress_export( definition.project, definition.wxr_file_path, definition.uploads_folder_path ) ensures: definition.last_analysis = report ensures: definition.updated_at = now } rule ResolveImportConflict { when: ResolveImportConflictRequested(definition, kind, item_name, resolution) requires: definition.last_analysis != null requires: resolution = ignore or resolution = overwrite or resolution = import ensures: definition.last_analysis = with_conflict_resolution( definition.last_analysis, kind, item_name, resolution ) ensures: definition.updated_at = now } rule MapImportTaxonomy { when: MapImportTaxonomyRequested(definition, kind, source_name, target_name) requires: definition.last_analysis != null ensures: definition.last_analysis = with_taxonomy_mapping( definition.last_analysis, kind, source_name, target_name ) ensures: definition.updated_at = now } rule AutoMapImportTaxonomy { when: AutoMapImportTaxonomyRequested(definition, model) requires: definition.last_analysis != null ensures: definition.last_analysis = with_ai_taxonomy_mappings(definition.last_analysis, model) ensures: definition.updated_at = now } rule ExecuteWordPressImport { when: ExecuteWordPressImportRequested(definition) requires: definition.last_analysis != null requires: importable_count(definition.last_analysis) > 0 ensures: WordPressImportExecuted(definition, result) @guidance -- New posts/pages use the source author or project default, preserve -- source timestamps, taxonomy and published/draft status, and route -- through normal post persistence/publishing. Pages also receive the -- page category. Imported media route through normal media import, -- preserve description as alt text, and link to an imported parent post -- when its WordPress parent is known. Missing media are skipped. } rule DeleteImportDefinition { when: DeleteImportDefinitionRequested(definition) ensures: not exists definition } invariant WxrConversionParity { -- A valid export requires an RSS channel and extracts site metadata, posts, -- pages, attachments, categories and tags. Post HTML is converted to -- Markdown and WordPress shortcodes become bDS [[macro ...]] calls. } invariant AnalysisClassification { -- Post/page checksums are lower-hex SHA-256 of converted Markdown; media -- checksums are lower-hex MD5 of source bytes, matching bDS2. -- Posts/pages: same slug and checksum => update; same slug with different -- content => conflict; checksum matching another post => content duplicate; -- otherwise new. Media applies the same rules by original filename and -- checksum, with missing selected-upload files classified as missing. } invariant CoreEngineReuse { -- Execution uses the ordinary tag, post, publishing, media, filesystem, -- search, embedding and domain-event paths. It does not create a parallel -- import-only data model or bypass normal side effects. } invariant RecoverableExecution { -- Taxonomy, posts, media and pages execute in that order. Each phase is -- committed in batches of config.transaction_batch_size. A failure rolls -- back the current batch and stops with a structured error; completed -- batches remain available for inspection and a later retry. } invariant SafeUntrustedInput { -- Unknown XML elements and report keys are ignored without creating -- unbounded runtime identifiers. Malformed XML or a missing RSS channel -- fails analysis without changing project content. Progress/reporting -- callback failures never abort otherwise valid analysis or execution. }