Files
RuDS/specs/editor_misc.allium

393 lines
15 KiB
Plaintext

-- allium: 1
-- bDS Miscellaneous Editor Views
-- Scope: UI content area — dashboard, menu editor, metadata diff, git diff,
-- documentation, validation, find duplicates, import analysis
-- Distilled from: Editor.tsx (Dashboard), MenuEditorView.tsx,
-- MetadataDiffPanel.tsx, ImportAnalysisView.tsx
-- Describes the layout and behaviour of smaller editor views that don't
-- warrant their own spec file.
use "./tabs.allium" as tabs
-- ─── External surfaces ──────────────────────────────────────
surface User {
provides: ImportAnalyzeRequested(definition_id, file_path)
provides: ImportExecuteRequested(definition_id)
provides: SiteValidationScanRequested()
provides: SiteValidationApplyRequested(report)
provides: TranslationValidationScanRequested()
provides: TranslationValidationFixRequested(report)
provides: DuplicateSearchRequested()
provides: DuplicatePairDismissed(post_id_a, post_id_b)
provides: DuplicatePairsBatchDismissed(pair_ids)
provides: MenuSaveRequested()
provides: MenuItemAdded(kind, data)
provides: MenuItemDeleted(item_id)
provides: MenuItemMoved(item_id, direction)
}
-- ─── Dashboard (no tab active) ───────────────────────────────
-- Shown as default/welcome view when no entity tab is active.
-- Single centered column.
value Dashboard {
title: String
subtitle: String
stats: DashboardStats
timeline: DashboardTimeline
tag_cloud: DashboardTagCloud
category_cloud: DashboardCategoryCloud
recent_posts: List<DashboardRecentPost>
}
value DashboardStats {
-- Three stat cards side by side:
total_posts: Integer
published_count: Integer
draft_count: Integer
archived_count: Integer -- shown only if > 0
media_count: Integer
image_count: Integer
total_media_size: String -- formatted B/KB/MB/GB
tag_count: Integer
category_count: Integer
}
value DashboardTimeline {
-- Bar chart of posts over time
-- Last 12 months that have data
-- Each bar: count label on top, month abbreviation + year below
-- Bar height proportional to max count
months: List<DashboardTimelineMonth>
}
value DashboardTimelineMonth {
label: String -- month abbreviation
year: Integer
count: Integer
}
value DashboardTagCloud {
-- Up to 40 tags, sorted alphabetically
-- Font size scaled 11px-22px based on post count
-- Tags with colours get coloured background with contrast text
-- Hover title shows post count
-- "and N more" text if > 40 tags
tags: List<DashboardTag>
overflow_count: Integer?
}
value DashboardTag {
name: String
count: Integer
color: String?
}
value DashboardCategoryCloud {
-- All categories as badge-like tags
-- Each shows category name + count
categories: List<DashboardCategory>
}
value DashboardCategory {
name: String
count: Integer
}
value DashboardRecentPost {
-- Last 5 posts by updatedAt descending
-- Each row: title (or "Untitled"), status badge (draft/published), date
-- Single-click: preview tab, double-click: pin tab
post_id: String
title: String
status: String
date: String
}
config {
dashboard_max_tags: Integer = 40
dashboard_tag_min_font: Integer = 11
dashboard_tag_max_font: Integer = 22
dashboard_recent_count: Integer = 5
dashboard_timeline_months: Integer = 12
}
-- ─── Menu editor view ────────────────────────────────────────
-- Visual editor for the OPML navigation menu (meta/menu.opml).
-- See menu.allium for data model.
-- Layout: toolbar at top, tree view of menu items below.
-- Each item row: drag handle, icon, label, type badge, action buttons.
-- Nested items indented to show hierarchy.
-- ─── Menu editor actions ────────────────────────────────────
rule MenuAddItem {
when: MenuItemAdded(kind, data)
-- kind = page: opens lazy-loaded page picker (posts with "page" category)
-- kind = category_archive: opens lazy-loaded category picker
-- kind = submenu: creates empty container node for nesting children
-- kind = home: always available, maximum one allowed
ensures: MenuTreeUpdated()
}
rule MenuSave {
when: MenuSaveRequested()
-- Serializes tree to OPML 2.0, writes meta/menu.opml
ensures: MenuFileWritten()
}
rule MenuMoveItem {
when: MenuItemMoved(item_id, direction)
-- direction = up | down | indent | unindent
-- Up/Down: reorder within same nesting level
-- Indent: nest under previous sibling (becomes child)
-- Unindent: move to parent's level (becomes next sibling of parent)
requires: not is_home_item(item_id)
-- Home item is protected: cannot be moved or deleted
ensures: MenuTreeUpdated()
}
rule MenuDeleteItem {
when: MenuItemDeleted(item_id)
requires: not is_home_item(item_id)
-- Removes item and all children, no confirmation dialog
ensures: MenuTreeUpdated()
}
-- Drag-and-drop reorder:
-- Drag handle on each item
-- Auto-expand collapsed submenus on hover (450ms delay)
-- Drop indicators show target position and nesting level
config {
menu_drag_expand_delay: Integer = 450
}
-- ─── Metadata diff view ──────────────────────────────────────
-- Shows DB vs filesystem differences for all entity types.
-- See metadata_diff.allium for diff field definitions.
-- Layout: scan button at top, results grouped by entity type below.
-- Each diff entry: entity name, field name, DB value, file value,
-- two sync buttons (DB->File, File->DB).
-- ─── Metadata diff actions ──────────────────────────────────
-- Scan: runs 4 parallel scans (posts, media, scripts, templates)
-- Compares DB records against filesystem files field by field
-- Results grouped by entity type, sorted by entity name
-- Shows count of differences per entity type in section header
-- Per-field sync:
-- DB -> File button: overwrites file field with DB value
-- File -> DB button: overwrites DB field with file value
-- No confirmation dialog for individual field syncs
-- Button disappears after successful sync (field now matches)
-- Orphan file import:
-- Files on disk with no matching DB record shown separately
-- "Import" action creates DB record from file metadata
-- Appears at bottom of each entity type section
-- ─── Git diff view ────────────────────────────────────────────
-- Renders diff for a file (working tree vs HEAD) or a commit.
-- File diff: id = "git-diff:{filePath}"
-- Commit diff: id = "git-diff:commit:{commitHash}"
-- Supports inline and side-by-side diff display modes (from editor settings).
-- No actions beyond viewing — changes are managed via git sidebar.
-- ─── Documentation views ─────────────────────────────────────
-- documentation: renders DOCUMENTATION.md as styled HTML
-- api_documentation: renders API.md as styled HTML
-- ─── Site validation view ───────────────────────────────────
-- Compares sitemap.xml to generated HTML files on disk.
-- Detects missing pages, orphan HTML, and stale content.
value SiteValidationReport {
expected_url_count: Integer
existing_html_count: Integer
missing_url_paths: List<String> -- in sitemap, no HTML on disk
extra_url_paths: List<String> -- HTML on disk, not in sitemap
updated_post_url_paths: List<String> -- source .md newer than HTML
}
-- Layout: scan button, summary line, three URL list sections.
-- Summary: "Expected URLs: N — Existing HTML URLs: N — Missing: N — Extra: N — Updated: N"
-- Each section: heading, list of URL paths, or "None found".
rule SiteValidationScan {
when: SiteValidationScanRequested()
-- Parses <loc> entries from sitemap.xml into expected URL set
-- Scans HTML output dir for index.html files (zero-byte = missing)
-- Compares source .md mtime against generated HTML mtime
ensures: SiteValidationReport
}
rule SiteValidationApply {
when: SiteValidationApplyRequested(report)
-- Classifies affected paths into generation sections (core, single, category, tag, date)
-- Renders only affected sections in parallel
-- Deletes extra HTML files, removes empty directories
-- Regenerates calendar if anything changed
-- Rebuilds search index if anything rendered or deleted
-- Toast: "Validation applied: N rendered, N deleted"
ensures: GenerateSiteRequested(sections: report.affected_sections)
}
-- ─── Translation validation view ───────────────────────────
-- Checks translation integrity across DB rows and filesystem files.
-- NOT a matrix view — it is a list of issues found.
value TranslationValidationReport {
checked_database_row_count: Integer
checked_filesystem_file_count: Integer
invalid_database_rows: List<TranslationValidationIssue>
invalid_filesystem_files: List<TranslationValidationIssue>
}
value TranslationValidationIssue {
issue: String
-- Issue kinds:
-- missing_source_post: translationFor points to nonexistent post
-- same_language_as_canonical: translation language matches source post language
-- do_not_translate_has_translations: source post is doNotTranslate but has translations
-- content_in_database: published translation still has content in DB (should be on disk)
translation_id: String?
translation_for: String
canonical_language: String?
translation_language: String
title: String?
file_path: String?
}
-- Layout: Revalidate + Fix Issues buttons, summary line, two issue sections.
-- Summary: "Checked DB rows: N — Checked files: N — Invalid DB rows: N — Invalid files: N"
-- Each issue rendered as card with coloured left border.
-- Card shows: issue label, source post ID, translation ID, title, languages, file path.
rule TranslationValidationScan {
when: TranslationValidationScanRequested()
-- Database pass: checks all translation rows for integrity issues
-- Filesystem pass: scans posts/ for translation .md files, checks frontmatter
ensures: TranslationValidationReport
}
rule TranslationValidationFix {
when: TranslationValidationFixRequested(report)
-- content_in_database: flushes content to .md file, sets content = null in DB
-- missing_source_post | same_language_as_canonical | do_not_translate:
-- DB issues: deletes translation row
-- Filesystem issues: deletes the .md file
-- After fix: automatically re-validates
-- Toast: "Deleted N DB rows and N files, flushed N translations to disk"
ensures: TranslationValidationScan
}
-- ─── Find duplicates view ──────────────────────────────────
-- Uses embedding vectors to find semantically similar posts.
-- Gate: requires semanticSimilarityEnabled in project metadata.
-- If disabled: shows "Semantic similarity is not enabled" message.
value DuplicateSearchResult {
pairs: List<DuplicatePair>
}
value DuplicatePair {
post_id_a: String
title_a: String
post_id_b: String
title_b: String
similarity: Decimal -- 0.0 to 1.0
exact_match: Boolean -- true if titles + content identical
}
-- Layout: header, count, paginated pair list (500 per page).
-- Each pair row: checkbox, post A title (clickable), arrow, post B title (clickable),
-- similarity badge ("Exact duplicate" or "N% similar"), Dismiss button.
-- Exact matches styled distinctly. Batch controls: Check All, Uncheck All,
-- Dismiss Checked (N). Refresh button.
rule DuplicateSearch {
when: DuplicateSearchRequested()
requires: semantic_similarity_enabled
-- Loads USearch vector index for project
-- For each indexed post: search 21 nearest neighbors
-- similarity = max(0, 1 - distance)
-- Filter: similarity >= threshold, exclude dismissed pairs
-- For 100% embedding similarity: load post bodies, compare title+content
-- If identical: exact_match = true
-- Sort: exact matches first, then descending similarity
ensures: DuplicateSearchResult
}
rule DuplicateDismiss {
when: DuplicatePairDismissed(post_id_a, post_id_b)
-- Inserts into dismissed_duplicate_pairs with canonical ID ordering
-- Excluded from future searches
-- See schema.allium DismissedDuplicatePair
ensures: PairDismissed(post_id_a, post_id_b)
}
rule DuplicateBatchDismiss {
when: DuplicatePairsBatchDismissed(pair_ids)
-- Batch insert in chunks of 100
ensures: for pair in pair_ids: PairDismissed(pair)
}
config {
duplicate_similarity_threshold: Decimal = 0.92
duplicate_page_size: Integer = 500
duplicate_neighbor_count: Integer = 21
}
-- ─── Import analysis view ───────────────────────────────────
-- Editor for WXR (WordPress eXtended RSS) import definitions.
-- Keyed by import definition ID. Opened as always-pinned tab.
-- Workflow: Select File -> Analyze -> Resolve Conflicts -> Execute
-- Layout: header (definition name), file selector, analysis results,
-- conflict resolution panel, taxonomy mapping, execute button.
-- ─── Import analysis actions ────────────────────────────────
rule ImportSelectAndAnalyze {
when: ImportAnalyzeRequested(definition_id, file_path)
-- Parses WXR XML file
-- Extracts: posts, pages, media, tags, categories, authors
-- Shows summary counts per entity type
-- Identifies conflicts: duplicate slugs, existing categories/tags
}
-- Conflict resolution:
-- Per-item dropdown: Import (create new), Skip (ignore), Merge (combine)
-- Default: Import for new items, Skip for existing matches
-- Taxonomy mapping:
-- Source categories/tags mapped to existing project categories/tags
-- Dropdown per source taxonomy term -> target term or "Create New"
-- AI taxonomy analysis (optional):
-- Gate: airplane mode check
-- Suggests mappings based on name similarity via title model
rule ImportExecute {
when: ImportExecuteRequested(definition_id)
-- No confirmation dialog — executes immediately
-- Processes items per conflict resolution settings
-- Creates posts, media, tags, categories as needed
-- Summary with counts shown in import view on completion
-- Created entities appear in sidebar immediately (store updated)
}