chore: hardened editor specs

This commit is contained in:
2026-04-05 13:44:45 +02:00
parent 3d89e8ea22
commit 6e34f5de1c
8 changed files with 1294 additions and 510 deletions

View File

@@ -5,55 +5,40 @@
-- Describes the layout and behaviour of the chat panel.
use "./i18n.allium" as i18n
-- ─── Chat panel ───────────────────────────────────────────────
-- Three-region vertical layout: header, message area, input area.
-- API Key Required screen (shown when no API key):
-- Key icon, title, description, "Open Settings" button
-- Header:
-- Left: conversation title (or "New Chat"), CSS ellipsis on overflow
-- Right: model selector button + dropdown. Groups models by provider.
-- Message area:
-- Welcome screen (no messages, not streaming): robot icon, tips
-- Message list: each has avatar (user=person, assistant=robot), role label, text
-- User messages: plain text
-- Assistant messages: rendered as GFM Markdown
-- External images blocked (CSP), shown as links
-- Tool markers: checkmark/dot, tool name, truncated args (30 char strings)
-- Streaming: accumulating markdown + tool markers, thinking dots animation
-- Auto-scrolls to bottom on new messages
-- Input area:
-- Abort/Stop button (only during streaming, square stop icon)
-- Auto-growing textarea (max 200px). Enter sends, Shift+Enter newline
-- Send button (up-arrow icon), disabled when empty or streaming
-- Token usage tracked per conversation (displayed in status bar, not chat panel).
-- Per-conversation model override via header selector.
config {
chat_tool_args_max_length: Integer = 30
chat_input_max_height: Integer = 200
value ChatPanelView {
conversation_id: String?
needs_api_key: Boolean
title: String -- conversation title or "New Chat"
selected_model_id: String?
messages: List<ChatMessage>
is_streaming: Boolean
input_text: String
}
-- ─── Model selector dropdown ──────────────────────────────────
value ChatMessage {
role: String -- user | assistant | system
content: String -- user: plain text; assistant: GFM markdown
tool_markers: List<ToolMarker>
is_streaming: Boolean -- true while accumulating
}
value ToolMarker {
tool_name: String
args_preview: String -- string args truncated to config.chat_tool_args_max_length
is_complete: Boolean -- checkmark when done, dot when in-progress
}
value ModelSelectorDropdown {
-- Dropdown in chat panel header for per-conversation model override.
-- Groups models by provider using section headers.
-- Each entry: model display name.
-- Expandable details per model: context window, max output tokens, pricing.
-- Selected model persisted with conversation record.
-- Changing model mid-conversation applies to subsequent messages only.
groups: List<ModelProviderGroup>
selected_model_id: String?
}
value ModelProviderGroup {
provider_name: String -- e.g. "OpenAI", "Ollama", "LM Studio"
provider_name: String -- e.g. "OpenAI", "Ollama", "LM Studio"
models: List<ModelEntry>
}
@@ -64,25 +49,82 @@ value ModelEntry {
max_output_tokens: Integer
}
-- ─── Chat panel actions ─────────────────────────────────────
config {
chat_tool_args_max_length: Integer = 30
chat_input_max_height: Integer = 200
}
-- Model selector:
-- Dropdown in header, groups models by provider (optgroup)
-- Selection is per-conversation override, persisted with conversation
-- Changing model mid-conversation applies to subsequent messages only
surface ChatPanelSurface {
context panel: ChatPanelView
-- Streaming behaviour:
-- Messages accumulate in real-time (markdown rendered incrementally)
-- Tool call markers: checkmark icon (done) or dot (in-progress),
-- tool name, args truncated to 30 chars for string values
-- Thinking animation (three dots) during LLM processing
exposes:
panel.needs_api_key
panel.title
panel.selected_model_id
panel.is_streaming
panel.input_text
for msg in panel.messages:
msg.role
msg.content
msg.is_streaming
for tm in msg.tool_markers:
tm.tool_name
tm.args_preview
tm.is_complete
-- Stop/Abort:
-- Square stop icon button, visible only during active streaming
-- Stops current generation, partial message preserved as-is
provides:
ChatSendMessage(panel.conversation_id, panel.input_text)
when panel.input_text != "" and not panel.is_streaming
ChatAbortStreaming(panel.conversation_id)
when panel.is_streaming
ChatSelectModel(panel.conversation_id, model_id)
ChatOpenSettings()
when panel.needs_api_key
-- Assistant action dispatch:
-- Assistant tool calls can trigger navigation actions:
-- open_post(id), open_media(id), open_settings(), etc.
-- Actions dispatched through store, same as user clicks
-- Navigation actions open tabs with pin intent
@guarantee ApiKeyRequiredScreen
-- Shown when needs_api_key is true.
-- Key icon, title, description text, "Open Settings" button.
-- No chat functionality available until API key is set.
@guarantee HeaderLayout
-- Left: conversation title (or "New Chat"), CSS ellipsis on overflow.
-- Right: model selector button opening dropdown.
@guarantee ModelSelectorDropdown
-- Dropdown groups models by provider (section headers).
-- Each entry: model display name.
-- Expandable details: context window, max output tokens.
-- Selection is per-conversation override, persisted with conversation.
-- Changing model mid-conversation applies to subsequent messages only.
@guarantee WelcomeScreen
-- Shown when no messages and not streaming.
-- Robot icon, title, description, 5 tip bullet points.
@guarantee MessageRendering
-- User messages: plain text.
-- Assistant messages: rendered as GFM Markdown.
-- External images blocked (CSP), shown as links.
-- Tool markers: checkmark (done) or dot (in-progress) icon,
-- tool name, args truncated to config.chat_tool_args_max_length for strings.
-- Streaming: accumulating markdown + tool markers, thinking dots animation.
@guarantee AutoScroll
-- Message area auto-scrolls to bottom on new messages.
@guarantee InputArea
-- Abort/Stop button: square stop icon, visible only during streaming.
-- Auto-growing textarea: max config.chat_input_max_height px.
-- Enter sends message. Shift+Enter inserts newline.
-- Send button: up-arrow icon, disabled when input is empty or streaming.
@guarantee AssistantActionDispatch
-- Assistant tool calls can trigger navigation actions:
-- open_post(id), open_media(id), open_settings(), etc.
-- Actions dispatched through store, same as user clicks.
-- Navigation actions open tabs with pin intent.
@guarantee TokenTracking
-- Token usage tracked per conversation.
-- Displayed in status bar, not in the chat panel itself.
}

View File

@@ -9,73 +9,31 @@
use "./media.allium" as media
use "./i18n.allium" as i18n
-- ─── External surfaces ──────────────────────────────────────
surface User {
provides: MediaAIImageAnalysisRequested(media_id)
provides: MediaDetectLanguageRequested(media_id)
provides: MediaTranslateMetadataRequested(media_id, target_language)
provides: MediaReplaceFileRequested(media_id)
provides: MediaDeleteRequested(media_id)
provides: MediaLinkToPostRequested(media_id)
provides: MediaTranslationEditClicked(media_id, language)
provides: MediaTranslationRefreshClicked(media_id, language)
provides: MediaTranslationDeleteClicked(media_id, language)
}
-- ─── Media editor ─────────────────────────────────────────────
-- Single column: preview area on top, metadata form below.
-- Header bar:
-- Media display name
-- Actions: Quick Actions dropdown, Replace File, Save, Delete
-- Quick Actions:
-- AI Image Analysis (image/* types only) - suggests title, alt, caption
-- Detect Language (from metadata text)
-- Translate Metadata (opens translation target modal)
-- Preview area:
-- Images: rendered via bds-media:// protocol with cache-busting timestamp
-- Non-images: SVG file icon placeholder + original filename text
-- Metadata form fields:
-- File Name (read-only), MIME Type (read-only),
-- Size (formatted as KB) + Dimensions (W x H, only if width/height exist),
-- Title (editable), Alt Text (editable), Caption (textarea 3 rows),
-- Tags (comma-separated text input), Author, Language (select)
-- Translations section (shown only when language is set):
-- List of existing translations: flag + language name + title
-- Per-translation: click to edit, refresh button, delete button
-- "No translations" message when empty
-- Linked Posts section:
-- "Link to Post" button opens post picker overlay
-- Post picker: search input, up to 10 unlinked posts, "and N more" if >10
-- List of currently linked posts (clickable -> navigates), unlink button
-- "Not linked to any posts" when empty
value MediaEditorView {
media_id: String
is_image: Boolean
file_name: String -- originalName, read-only
mime_type: String
file_size: String -- formatted
dimensions: String? -- "W x H" if available
title: String?
alt_text: String?
caption: String?
tags: String? -- comma-separated
author: String?
language: String?
mime_type: String -- read-only
file_size: String -- formatted, read-only
dimensions: String? -- "W x H" if width/height exist, read-only
title: String? -- editable text input
alt_text: String? -- editable text input
caption: String? -- editable textarea (3 rows)
tags: String? -- comma-separated text input
author: String? -- editable text input
language: String? -- select from supported languages
translations: List<MediaTranslationItem>
linked_posts: List<LinkedPostItem>
}
value MediaTranslationItem {
language: String
flag_emoji: String
title: String?
alt_text: String?
caption: String?
}
value LinkedPostItem {
@@ -83,18 +41,7 @@ value LinkedPostItem {
title: String
}
-- No auto-save; explicit Save button required.
-- Replace File opens a native file dialog.
-- Delete shows confirmation with references (linked posts).
-- ─── Post picker overlay ────────────────────────────────────
value PostPickerOverlay {
-- Inline overlay positioned near "Link to Post" button (media editor).
-- Not a modal — positioned as dropdown below trigger button.
-- Search input filtering posts by title (only unlinked posts shown).
-- Up to 10 results displayed. "and N more" text if total exceeds 10.
-- Click result: links media to selected post, closes overlay.
search_query: String
results: List<PostPickerResult>
overflow_count: Integer? -- shown as "and N more" if > 0
@@ -105,6 +52,93 @@ value PostPickerResult {
title: String
}
config {
media_post_picker_max_results: Integer = 10
}
surface MediaEditorSurface {
context editor: MediaEditorView
exposes:
editor.file_name
editor.mime_type
editor.file_size
editor.dimensions when editor.dimensions != null
editor.is_image
editor.title
editor.alt_text
editor.caption
editor.tags
editor.author
editor.language
for t in editor.translations:
t.language
t.flag_emoji
t.title
for lp in editor.linked_posts:
lp.post_id
lp.title
provides:
MediaAIImageAnalysisRequested(editor.media_id)
when editor.is_image
MediaDetectLanguageRequested(editor.media_id)
MediaTranslateMetadataRequested(editor.media_id, target_language)
MediaReplaceFileRequested(editor.media_id)
MediaSaveRequested(editor.media_id)
MediaDeleteRequested(editor.media_id)
MediaLinkToPostRequested(editor.media_id)
MediaTranslationEditClicked(editor.media_id, language)
MediaTranslationRefreshClicked(editor.media_id, language)
MediaTranslationDeleteClicked(editor.media_id, language)
MediaUnlinkPostRequested(editor.media_id, post_id)
@guarantee HeaderLayout
-- Header bar with media display name.
-- Actions (right side): Quick Actions dropdown, Replace File button,
-- Save button, Delete button (danger style).
@guarantee QuickActionsDropdown
-- Dropdown menu in header with three entries:
-- AI Image Analysis (robot icon) — only shown for image/* MIME types.
-- Detect Language (magnifier icon).
-- Translate Metadata (globe icon).
@guarantee PreviewArea
-- Images: rendered via bds-media:// protocol with cache-busting timestamp.
-- Non-images: SVG file icon placeholder + original filename text.
@guarantee MetadataForm
-- Form fields in order:
-- File Name (disabled input), MIME Type (disabled input),
-- Size + Dimensions row (disabled inputs),
-- Title (text input), Alt Text (text input), Caption (textarea, 3 rows),
-- Tags (comma-separated text input), Author (text input),
-- Language (select from supported languages).
@guarantee TranslationsSection
-- Shown only when language is set.
-- List of existing translations: flag emoji + language name + title.
-- Per-translation actions: click to edit inline, refresh button, delete button.
-- "No translations" message when list is empty.
@guarantee LinkedPostsSection
-- "Link to Post" button opens inline post picker overlay.
-- List of currently linked posts with document icon. Click navigates to post tab.
-- Per-post unlink button (×).
-- "Not linked to any posts" message when list is empty.
@guarantee PostPickerOverlay
-- Inline overlay positioned near "Link to Post" button (not a modal).
-- Search input filtering unlinked posts by title.
-- Up to config.media_post_picker_max_results results displayed.
-- "and N more" text when total exceeds limit.
-- Click result: links media to selected post, closes overlay.
@guarantee NoAutoSave
-- Unlike the post editor, media editor requires explicit Save button.
}
-- ─── Media editor actions ─────────────────────────────────────
rule MediaAIImageAnalysis {
@@ -161,7 +195,8 @@ rule MediaLinkToPost {
when: MediaLinkToPostRequested(media_id)
-- Opens inline post picker overlay (not a modal, positioned near button)
-- Search input filtering unlinked posts by title
-- Up to 10 results shown, "and N more" text if results exceed 10
-- Up to config.media_post_picker_max_results results shown
-- "and N more" text if results exceed limit
-- Click links media to selected post (updates sidecar linkedPostIds)
-- Linked posts list refreshes immediately
}

View File

@@ -9,29 +9,11 @@
-- 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)
}
use "./i18n.allium" as i18n
-- ─── Dashboard (no tab active) ───────────────────────────────
-- Shown as default/welcome view when no entity tab is active.
-- Single centered column.
value Dashboard {
title: String
@@ -44,7 +26,6 @@ value Dashboard {
}
value DashboardStats {
-- Three stat cards side by side:
total_posts: Integer
published_count: Integer
draft_count: Integer
@@ -57,10 +38,6 @@ value DashboardStats {
}
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>
}
@@ -71,13 +48,8 @@ value DashboardTimelineMonth {
}
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?
overflow_count: Integer? -- "and N more" when > config.dashboard_max_tags
}
value DashboardTag {
@@ -87,8 +59,6 @@ value DashboardTag {
}
value DashboardCategoryCloud {
-- All categories as badge-like tags
-- Each shows category name + count
categories: List<DashboardCategory>
}
@@ -98,13 +68,10 @@ value DashboardCategory {
}
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
title: String -- "Untitled" as fallback
status: String -- draft | published
date: String -- locale-formatted
}
config {
@@ -115,16 +82,144 @@ config {
dashboard_timeline_months: Integer = 12
}
surface DashboardSurface {
context dash: Dashboard
exposes:
dash.title
dash.subtitle
dash.stats.total_posts
dash.stats.published_count
dash.stats.draft_count
dash.stats.archived_count when dash.stats.archived_count > 0
dash.stats.media_count
dash.stats.image_count
dash.stats.total_media_size
dash.stats.tag_count
dash.stats.category_count
for m in dash.timeline.months:
m.label
m.year
m.count
for t in dash.tag_cloud.tags:
t.name
t.count
t.color
dash.tag_cloud.overflow_count when dash.tag_cloud.overflow_count != null
for c in dash.category_cloud.categories:
c.name
c.count
for rp in dash.recent_posts:
rp.title
rp.status
rp.date
provides:
DashboardRecentPostClicked(post_id, single)
DashboardRecentPostClicked(post_id, double)
@guarantee StatCards
-- Three stat cards side by side.
-- Posts card: total number, breakdown tags (published/drafts/archived if > 0).
-- Media card: count, images count, total size (formatted bytes).
-- Tags card: count, categories count.
@guarantee TimelineChart
-- Bar chart of posts over last config.dashboard_timeline_months months that have data.
-- Each bar: count label on top, month abbreviation + year below.
-- Bar height proportional to max count.
@guarantee TagCloud
-- Up to config.dashboard_max_tags tags, sorted alphabetically.
-- Font size scaled config.dashboard_tag_min_font to config.dashboard_tag_max_font px
-- based on post count.
-- Tags with colours get coloured background with contrast text.
-- Hover title shows post count.
-- "and N more" text when overflow_count > 0.
@guarantee CategoryCloud
-- All categories as badge-like tags.
-- Each shows category name + count.
@guarantee RecentPosts
-- Last config.dashboard_recent_count posts by updatedAt descending.
-- Each row: title (or "Untitled"), status badge (draft/published), date.
-- Single-click: preview tab. Double-click: pin tab.
}
-- ─── 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.
value MenuEditorView {
items: List<MenuTreeItem>
}
-- ─── Menu editor actions ────────────────────────────────────
value MenuTreeItem {
item_id: String
kind: String -- home | page | category_archive | submenu
label: String
children: List<MenuTreeItem>
is_home: Boolean -- true for the home item (protected)
}
surface MenuEditorSurface {
context menu: MenuEditorView
exposes:
for item in menu.items:
item.kind
item.label
item.children
item.is_home
provides:
MenuItemAdded(kind, data)
MenuSaveRequested()
MenuItemDeleted(item_id)
when not item.is_home
MenuItemMoved(item_id, direction)
when not item.is_home
@guarantee HeaderLayout
-- Title + description text.
@guarantee Toolbar
-- 8 icon buttons with tooltips:
-- Add Entry (+), Save (floppy), Add Category Archive,
-- Move Up, Move Down, Indent, Unindent, Delete.
@guarantee TreeView
-- Drag-and-drop tree with items showing:
-- Drag handle, kind icon (home/page/category-archive/submenu SVGs),
-- title, selected row highlighting.
-- Nested items indented to show hierarchy.
@guarantee InlineEditing
-- When creating page item: inline PageInput with search
-- (filters posts in "page" category).
-- When creating category archive: inline CategoryInput with search/create.
-- Escape cancels, selection confirms.
@guarantee HomeItemProtection
-- Home item cannot be moved, reordered, or deleted.
-- Maximum one home item allowed.
@guarantee DragDrop
-- Drag handle on each item.
-- Auto-expand collapsed submenus on hover (config.menu_drag_expand_delay ms delay).
-- Drop indicators show target position and nesting level.
@guarantee MoveDirections
-- 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).
}
config {
menu_drag_expand_delay: Integer = 450
}
rule MenuAddItem {
when: MenuItemAdded(kind, data)
@@ -144,11 +239,7 @@ rule MenuSave {
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()
}
@@ -159,71 +250,187 @@ rule MenuDeleteItem {
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).
value MetadataDiffView {
is_scanning: Boolean
active_entity_tab: String -- posts | media | scripts | templates
diff_stats: MetadataDiffStats
field_summaries: List<MetadataDiffFieldSummary>
items: List<MetadataDiffItem>
orphan_files: List<MetadataDiffOrphanFile>
}
-- ─── Metadata diff actions ──────────────────────────────────
value MetadataDiffStats {
total_posts: Integer
published_posts: Integer
draft_posts: Integer
media_files: Integer
scripts: Integer
templates: Integer
}
-- 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
value MetadataDiffFieldSummary {
field_name: String
diff_count: Integer
}
-- 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)
value MetadataDiffItem {
entity_name: String
entity_type: String
file_missing: Boolean -- badge shown when file not found
field_diffs: List<MetadataDiffField>
}
-- 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
value MetadataDiffField {
field_name: String
db_value: String?
file_value: String?
}
value MetadataDiffOrphanFile {
file_path: String
entity_type: String
}
surface MetadataDiffSurface {
context diff: MetadataDiffView
exposes:
diff.is_scanning
diff.active_entity_tab
diff.diff_stats
for fs in diff.field_summaries:
fs.field_name
fs.diff_count
for item in diff.items:
item.entity_name
item.file_missing
for fd in item.field_diffs:
fd.field_name
fd.db_value
fd.file_value
for orphan in diff.orphan_files:
orphan.file_path
provides:
MetadataDiffScanRequested()
MetadataDiffSyncFieldToFile(entity_name, field_name)
MetadataDiffSyncFieldToDb(entity_name, field_name)
MetadataDiffSyncAllFieldToFile(field_name)
MetadataDiffSyncAllFieldToDb(field_name)
MetadataDiffImportOrphan(file_path)
@guarantee HeaderLayout
-- Title + description text.
-- Stats row: 6 stat items (total posts, published, drafts, media, scripts, templates).
@guarantee ScanAction
-- Scan/Rescan button at top.
-- Progress bar + message during scan.
@guarantee EntityTabs
-- Tabs: Posts, Media, Scripts, Templates — each with badge count of diffs.
@guarantee FieldSummaryPills
-- Clickable filter pills per field, each with count.
-- Two bulk sync buttons per pill: DB→File and File→DB (syncs all items for that field).
@guarantee DiffItemCards
-- Per-item card: header (entity label + file-missing badge),
-- field rows (field name, DB value, file value),
-- two sync buttons per field (DB→File, File→DB).
-- Button disappears after successful sync (field now matches).
@guarantee OrphanFilesSection
-- Files on disk with no matching DB record shown at bottom of each entity tab.
-- "Import" button creates DB record from file metadata.
@guarantee NoConfirmation
-- Individual field syncs require no confirmation dialog.
}
-- ─── 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.
value GitDiffView {
diff_id: String
diff_type: String -- file | commit
display_mode: String -- inline | side_by_side (from editor settings)
}
surface GitDiffSurface {
context diff: GitDiffView
exposes:
diff.diff_id
diff.diff_type
diff.display_mode
@guarantee DiffDisplayModes
-- Supports inline and side-by-side diff display modes.
-- Mode comes from editor settings (Diff View Style).
@guarantee ReadOnly
-- 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 ───────────────────────────────────
surface DocumentationSurface {
@guarantee MarkdownRendering
-- Renders markdown file as styled HTML.
-- No edit actions. Read-only view.
}
-- Compares sitemap.xml to generated HTML files on disk.
-- Detects missing pages, orphan HTML, and stale content.
-- ─── Site validation view ───────────────────────────────────
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
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".
surface SiteValidationSurface {
context report: SiteValidationReport
exposes:
report.expected_url_count
report.existing_html_count
report.missing_url_paths
report.extra_url_paths
report.updated_post_url_paths
provides:
SiteValidationScanRequested()
SiteValidationApplyRequested(report)
when report.missing_url_paths.count > 0
or report.extra_url_paths.count > 0
or report.updated_post_url_paths.count > 0
@guarantee SummaryLine
-- "Expected URLs: N — Existing HTML URLs: N — Missing: N — Extra: N — Updated: N"
@guarantee UrlSections
-- Three sections: Missing URLs, Extra URLs, Updated URLs.
-- Each section: heading + list of URL paths, or "None found".
@guarantee ApplyAction
-- Apply button disabled when nothing to fix.
-- On apply: renders missing, deletes extra, re-renders updated.
-- Toast: "Validation applied: N rendered, N deleted".
}
rule SiteValidationScan {
when: SiteValidationScanRequested()
@@ -240,15 +447,11 @@ rule SiteValidationApply {
-- 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
@@ -271,10 +474,51 @@ value TranslationValidationIssue {
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.
surface TranslationValidationSurface {
context report: TranslationValidationReport
exposes:
report.checked_database_row_count
report.checked_filesystem_file_count
for issue in report.invalid_database_rows:
issue.issue
issue.translation_id
issue.translation_for
issue.canonical_language
issue.translation_language
issue.title
issue.file_path
for issue in report.invalid_filesystem_files:
issue.issue
issue.file_path
provides:
TranslationValidationScanRequested()
TranslationValidationFixRequested(report)
when report.invalid_database_rows.count > 0
or report.invalid_filesystem_files.count > 0
@guarantee SummaryLine
-- "Checked DB rows: N — Checked files: N — Invalid DB rows: N — Invalid files: N"
@guarantee IssueSections
-- Two sections: Database Issues, Filesystem Issues.
-- Each issue rendered as card with coloured left border.
-- Card shows: issue label, source post ID, translation ID, title, languages, file path.
@guarantee IssueTypes
-- same_language_as_canonical: translation language matches source.
-- do_not_translate_has_translations: source is doNotTranslate.
-- content_in_database: published translation has content in DB.
-- missing_source_post: translationFor references nonexistent post.
@guarantee FixAction
-- Revalidate button + Fix button (disabled when no issues).
-- Fix: content_in_database -> flush to .md file, set content null.
-- Other issues -> delete DB row or .md file.
-- After fix: automatically re-validates.
-- Toast: "Deleted N DB rows and N files, flushed N translations to disk".
}
rule TranslationValidationScan {
when: TranslationValidationScanRequested()
@@ -290,18 +534,14 @@ rule TranslationValidationFix {
-- 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>
has_more: Boolean -- pagination with config.duplicate_page_size per page
}
value DuplicatePair {
@@ -309,23 +549,67 @@ value DuplicatePair {
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
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.
config {
duplicate_similarity_threshold: Decimal = 0.92
duplicate_page_size: Integer = 500
duplicate_neighbor_count: Integer = 21
}
surface DuplicatesSurface {
context result: DuplicateSearchResult
exposes:
for pair in result.pairs:
pair.title_a
pair.title_b
pair.similarity
pair.exact_match
result.has_more
provides:
DuplicateSearchRequested()
DuplicatePairDismissed(post_id_a, post_id_b)
DuplicatePairsBatchDismissed(pair_ids)
DuplicatePostClicked(post_id)
DuplicateShowMoreRequested()
when result.has_more
@guarantee SemanticSimilarityGate
-- Requires semanticSimilarityEnabled in project metadata.
-- If disabled: shows "Semantic similarity is not enabled" message.
-- No search functionality available when disabled.
@guarantee ActionsBar
-- Refresh button, Check All, Uncheck All,
-- Dismiss Checked (with count), disabled when none checked.
@guarantee PairRows
-- Each row: checkbox, post A title (clickable -> opens tab),
-- arrow, post B title (clickable -> opens tab),
-- similarity badge (percentage or "Exact Match"),
-- Dismiss button.
-- Exact matches styled distinctly from similarity matches.
@guarantee Pagination
-- "Show More" button when has_more is true.
-- config.duplicate_page_size pairs per page.
@guarantee BatchDismiss
-- Batch insert in chunks of 100.
-- Dismissed pairs excluded from future searches.
}
rule DuplicateSearch {
when: DuplicateSearchRequested()
requires: semantic_similarity_enabled
-- Loads USearch vector index for project
-- For each indexed post: search 21 nearest neighbors
-- For each indexed post: search config.duplicate_neighbor_count nearest neighbors
-- similarity = max(0, 1 - distance)
-- Filter: similarity >= threshold, exclude dismissed pairs
-- Filter: similarity >= config.duplicate_similarity_threshold, exclude dismissed
-- For 100% embedding similarity: load post bodies, compare title+content
-- If identical: exact_match = true
-- Sort: exact matches first, then descending similarity
@@ -336,7 +620,6 @@ 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)
}
@@ -346,22 +629,139 @@ rule DuplicateBatchDismiss {
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.
value ImportAnalysisView {
definition_id: String
definition_name: String -- editable name input
uploads_folder_path: String? -- path display + Browse button
wxr_file_path: String? -- path display + Select & Analyze button
is_loading: Boolean
report: ImportAnalysisReport?
}
-- ─── Import analysis actions ────────────────────────────────
value ImportAnalysisReport {
site_info: ImportSiteInfo
post_stats: ImportEntityStats
page_stats: ImportEntityStats
media_stats: ImportMediaStats
category_stats: ImportTaxonomyStats
tag_stats: ImportTaxonomyStats
date_distribution: List<ImportYearDistribution>
conflicts: List<ImportConflict>
macros: List<ImportMacro>
}
value ImportSiteInfo {
title: String
url: String
language: String
source_file: String
}
value ImportEntityStats {
new_count: Integer
update_count: Integer
conflict_count: Integer
duplicate_count: Integer
}
value ImportMediaStats {
new_count: Integer
update_count: Integer
conflict_count: Integer
duplicate_count: Integer
missing_count: Integer
}
value ImportTaxonomyStats {
existing_count: Integer
mapped_count: Integer
new_count: Integer
}
value ImportYearDistribution {
year: Integer
post_count: Integer
media_count: Integer
}
value ImportConflict {
item_type: String -- post | page | media
item_name: String
resolution: String -- import | skip | merge
}
value ImportMacro {
name: String
usage_count: Integer
parameters: List<String>
validation_status: String -- valid | invalid | unknown
}
surface ImportAnalysisSurface {
context analysis: ImportAnalysisView
exposes:
analysis.definition_name
analysis.uploads_folder_path
analysis.wxr_file_path
analysis.is_loading
analysis.report when analysis.report != null
provides:
ImportAnalyzeRequested(analysis.definition_id, file_path)
when analysis.wxr_file_path != null
ImportExecuteRequested(analysis.definition_id)
when analysis.report != null
ImportConflictResolutionChanged(item_name, resolution)
ImportTaxonomyMappingChanged(source_term, target_term)
ImportAITaxonomyAnalysisRequested(analysis.definition_id)
@guarantee FileSelectors
-- Uploads folder: path display + Browse button (native folder dialog).
-- WXR file: path display + Select & Analyze button (native file dialog).
@guarantee LoadingState
-- Spinner + progress step + detail text during analysis.
@guarantee SiteInfoCard
-- Shows: site title, URL, language, source file path.
@guarantee StatCards
-- Posts (new/update/conflict/duplicate), Pages (same),
-- Media (new/update/conflict/duplicate/missing),
-- Categories (existing/mapped/new), Tags (existing/mapped/new).
@guarantee DateDistribution
-- Year-by-year bar charts for posts + media.
@guarantee ConflictsSection
-- Collapsible. Per-item dropdown: Import/Skip/Merge.
-- Default: Import for new items, Skip for existing matches.
@guarantee TaxonomySection
-- Collapsible. Category + tag pills.
-- Click pill to map to existing term. Inline edit with suggestion dropdown.
-- AI analyze button (with model selector dropdown).
-- Gate: airplane mode check for AI taxonomy analysis.
@guarantee MacrosSection
-- Collapsible. Discovered macros with usage counts, parameters,
-- validation status (valid/invalid/unknown badges).
@guarantee ExecuteAction
-- Execute button shows importable counts (tags, posts, media, pages).
-- Disabled if nothing to import.
-- Progress bar during execution (current/total, phase, detail, ETA).
-- No confirmation dialog — executes immediately.
@guarantee CreatedEntitiesRefresh
-- Created entities appear in sidebar immediately after execution.
}
rule ImportSelectAndAnalyze {
when: ImportAnalyzeRequested(definition_id, file_path)
@@ -371,17 +771,6 @@ rule ImportSelectAndAnalyze {
-- 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

View File

@@ -11,79 +11,16 @@ use "./tabs.allium" as tabs
use "./post.allium" as post
use "./i18n.allium" as i18n
-- ─── External surfaces ──────────────────────────────────────
surface User {
provides: PostAIAnalysisRequested(post_id)
provides: PostTranslateRequested(post_id, target_language)
provides: PostSaved(post_id)
provides: PostPublishRequested(post_id)
provides: PostDiscardRequested(post_id)
provides: PostDeleteRequested(post_id)
provides: PostInsertLinkRequested(post_id)
provides: PostInsertMediaRequested(post_id)
provides: PostGalleryRequested(post_id)
provides: ImageDroppedOnEditor(post_id, file_path)
provides: PostLanguageDetectRequested(post_id)
}
-- ─── Post editor ──────────────────────────────────────────────
-- Single vertical column. Three regions: header, body, footer.
-- Header bar:
-- Left: post title (or "Untitled") with dirty indicator dot
-- Right: status badge, auto-save indicator, Quick Actions dropdown, Publish/Delete buttons
-- Quick Actions:
-- AI Analysis (suggests title, excerpt, slug)
-- Translate Post (opens translation modal)
-- Collapsible Metadata section (starts expanded when title is empty):
-- Two-column layout. Left column: metadata fields. Right column: linked media panel.
-- Left column fields:
-- Title (text input), Tags (autocomplete chip input with embedding suggestions),
-- Author (text input),
-- Language (select + AI detect button), Do Not Translate (checkbox),
-- Slug (read-only), Categories (chip input), Template (select, if templates exist),
-- PostLinks (backlinks and outlinks)
--
-- Tag autocomplete:
-- Standard: matches existing tags by name prefix (case-insensitive)
-- If semanticSimilarityEnabled: also suggests tags from similar posts
-- via SuggestTags (see embedding.allium). Finds 10 similar posts,
-- collects their tags, weights by similarity, returns top 5.
-- Suggestions merged: prefix matches first, then embedding suggestions
-- Right column:
-- LinkedMediaPanel (media items linked to this post)
-- Language flags bar (inline with metadata toggle):
-- Row of flag emoji buttons for canonical language + each translation
-- Styles: status class (draft/published), active indicator
-- Clicking switches editor content to that language's draft
-- Collapsible Excerpt section:
-- Excerpt textarea (4 rows)
-- Editor Body:
-- Toolbar: "Content" label | mode toggle (Visual/Markdown/Preview) | action buttons
-- Action buttons (markdown mode only): Gallery, Insert Post Link, Insert Media
-- Visual mode: rich-text WYSIWYG editor
-- Markdown mode: code editor with markdown-with-macros language
-- Highlights [[macro ...]] double-bracket syntax
-- Word wrap on, minimap off, 14px font
-- Preview mode: iframe showing rendered preview
-- Supports drag-and-drop of images (imports media, inserts markdown)
-- Footer:
-- Three date stamps: Created, Updated, Published (published only if publishedAt exists)
-- Locale-formatted dates
value PostEditorView {
post_id: String
header: PostEditorHeader
metadata_expanded: Boolean
metadata: PostEditorMetadata
metadata_expanded: Boolean -- starts expanded when title is empty
excerpt_expanded: Boolean
editor_mode: String -- visual | markdown | preview
footer: PostEditorFooter
}
value PostEditorHeader {
@@ -93,20 +30,161 @@ value PostEditorHeader {
is_auto_saving: Boolean
}
value PostEditorMetadata {
title: String -- editable text input
tags: List<String> -- autocomplete chip input
author: String? -- text input
language: String? -- select from supported languages
do_not_translate: Boolean -- checkbox
slug: String -- read-only text input
categories: List<String> -- chip input
template_slug: String? -- select (shown only when templates exist)
post_links: PostLinksPanel
linked_media: List<LinkedMediaItem>
}
value PostLinksPanel {
backlinks: List<PostLinkReference> -- posts linking to this post
outlinks: List<PostLinkReference> -- posts this post links to
}
value PostLinkReference {
post_id: String
title: String
}
value LinkedMediaItem {
media_id: String
has_thumbnail: Boolean
name: String
sort_order: Integer
}
value PostEditorFooter {
created_at: String -- locale-formatted date
updated_at: String -- locale-formatted date
published_at: String? -- locale-formatted date, only when post was published
}
value TranslationFlag {
language: String
flag_emoji: String
status: String -- draft | published
is_active: Boolean -- true when this language is currently being edited
}
surface PostEditorSurface {
context editor: PostEditorView
exposes:
editor.header.title
editor.header.is_dirty
editor.header.status
editor.header.is_auto_saving
editor.metadata_expanded
editor.excerpt_expanded
editor.editor_mode
editor.footer.created_at
editor.footer.updated_at
editor.footer.published_at when editor.footer.published_at != null
provides:
PostAIAnalysisRequested(editor.post_id)
PostTranslateRequested(editor.post_id, target_language)
PostSaved(editor.post_id)
PostPublishRequested(editor.post_id)
when editor.header.status = draft
PostDiscardRequested(editor.post_id)
when editor.header.status = draft
PostDeleteRequested(editor.post_id)
PostInsertLinkRequested(editor.post_id)
when editor.editor_mode = markdown
PostInsertMediaRequested(editor.post_id)
when editor.editor_mode = markdown
PostGalleryRequested(editor.post_id)
ImageDroppedOnEditor(editor.post_id, file_path)
PostLanguageDetectRequested(editor.post_id)
@guarantee HeaderLayout
-- Header bar with two areas.
-- Left: title text with dirty indicator dot (●) when is_dirty is true.
-- Right: status badge, auto-save indicator (when saving),
-- Quick Actions dropdown, Publish button (only when draft, success style),
-- Discard button (only when draft), Delete button.
@guarantee QuickActionsDropdown
-- Dropdown menu in header with two entries:
-- AI Analysis (robot icon) — suggests title, excerpt, slug.
-- Translate Post (globe icon) — opens translation modal.
@guarantee MetadataSection
-- Collapsible section. Starts expanded when title is empty.
-- Two-column layout.
-- Left column: Title, Tags, Author, Language + detect button,
-- Do Not Translate checkbox, Slug (read-only), Categories,
-- Template (select, only when templates exist), PostLinks.
-- Right column: LinkedMediaPanel.
@guarantee TagAutocomplete
-- Tag input with autocomplete.
-- Standard: prefix match on existing tag names (case-insensitive).
-- When semanticSimilarityEnabled: also suggests tags from 10 similar posts,
-- weighted by similarity, top 5 shown.
-- Merged results: prefix matches first, then embedding suggestions.
@guarantee TranslationFlagsBar
-- Row of flag emoji buttons inline with metadata toggle.
-- One flag per language: canonical language + each translation.
-- Each flag shows status (draft/published) via CSS class.
-- Active flag highlighted. Click switches editor to that language's draft.
@guarantee ExcerptSection
-- Collapsible section with textarea (4 rows).
@guarantee EditorBodyToolbar
-- Toolbar: "Content" label, mode toggle (Visual/Markdown/Preview),
-- action buttons (markdown mode only): Gallery (with media count),
-- Insert Post Link, Insert Media.
@guarantee EditorModes
-- Visual: rich-text WYSIWYG editor.
-- Markdown: code editor with markdown-with-macros language,
-- highlighting [[macro ...]] syntax. Word wrap on, minimap off, 14px font.
-- Preview: iframe showing rendered preview.
@guarantee DragDropImages
-- Drop image file onto editor area triggers import chain.
@guarantee FooterLayout
-- Three date stamps: Created, Updated, Published (only when published_at exists).
-- Locale-formatted dates.
@guarantee LinkedMediaPanel
-- Right column of metadata showing media items linked to this post.
-- Actions: Import & Link (native file dialog), Link Existing (media picker),
-- Unlink (no confirmation), Reorder (drag handle), Click (opens media tab).
}
config {
post_auto_save_delay: Integer = 3000
-- milliseconds of idle before auto-save triggers
post_content_sample_length: Integer = 2000
}
invariant PostAutoSave {
-- Auto-saves after 3 seconds of idle in the editor
-- Also auto-saves on unmount/tab switch
-- Ctrl/Cmd+S triggers immediate save
-- Auto-saves after config.post_auto_save_delay ms of idle in the editor.
-- Also auto-saves on unmount/tab switch.
-- Ctrl/Cmd+S triggers immediate save.
-- Saves: title, content, excerpt, tags, categories, templateSlug, language.
}
invariant PostDirtyTracking {
-- Compares canonical draft + translation drafts against saved state
-- Dirty indicator shown in header and tab bar
-- Compares canonical draft + translation drafts against saved state.
-- Dirty indicator shown in header (●) and tab bar.
}
invariant PostEditorModePersistence {
-- Editor mode (visual/markdown/preview) persists per session.
-- Default mode comes from editor settings.
}
-- ─── Post editor actions ────────────────────────────────────
@@ -115,7 +193,7 @@ rule PostAIAnalysis {
when: PostAIAnalysisRequested(post_id)
-- Gate: airplane mode check (see action_patterns.allium AIOperationGating)
-- Uses title model (not default chat model)
-- Input: post title + excerpt + content (first 2000 chars)
-- Input: post title + excerpt + content (first config.post_content_sample_length chars)
-- Response: suggested title, excerpt, slug
-- Opens AISuggestionsModal with 3 fields:
-- Each field: current value, suggested value, accept checkbox
@@ -227,25 +305,3 @@ rule PostLanguageDetect {
-- Auto-sets post language field (no modal)
-- Triggers auto-save
}
-- Post editor mode switching:
-- Visual mode: rich-text WYSIWYG (TipTap in TS, not yet in Rust)
-- Markdown mode: code editor with markdown-with-macros highlighting
-- Preview mode: iframe with rendered HTML via preview server
-- Mode persists per-session, default from editor settings
-- Language tab switching (translation editing):
-- Flag emoji buttons in metadata bar for canonical + each translation
-- Clicking a flag loads that language's draft content into editor
-- Each flag shows status (draft/published) via CSS class
-- Active flag highlighted
-- LinkedMediaPanel (right column of metadata):
-- Shows media items linked to this post
-- Actions:
-- Import & Link: native file dialog, runs full import + link chain
-- Link Existing: opens media picker modal (search + grid)
-- Unlink: removes media-post link from sidecar (no confirmation)
-- Reorder: drag handle on each item
-- Click: opens media item in its own tab (preview intent)
-- Import & Link triggers: import -> thumbnail -> link -> optional AI analysis

View File

@@ -6,37 +6,83 @@
-- Describes the layout and behaviour of the script editor.
-- See script.allium for entity model.
-- ─── External surfaces ──────────────────────────────────────
surface User {
provides: ScriptSaveRequested(script_id)
provides: ScriptRunRequested(script_id)
provides: ScriptDeleteRequested(script_id)
}
use "./script.allium" as script
use "./i18n.allium" as i18n
-- ─── Script editor view ──────────────────────────────────────
-- Code editor for Lua scripts (macros, utilities, transforms).
value ScriptEditorView {
script_id: String
title: String -- editable text input
slug: String -- editable text input, auto-generated from title
kind: String -- select: utility | macro | transform
entrypoint: String -- select: dynamically discovered functions, "main" always first
enabled: Boolean -- checkbox
content: String -- code editor content
created_at: String -- locale-formatted date
updated_at: String -- locale-formatted date
}
-- Layout: header bar, code editor, no preview.
-- Header: script title (editable), kind badge, enabled toggle.
-- Editor: code editor with Lua syntax highlighting.
-- Toolbar: Run button, Save button, Delete button, entrypoint selector.
surface ScriptEditorSurface {
context editor: ScriptEditorView
exposes:
editor.title
editor.slug
editor.kind
editor.entrypoint
editor.enabled
editor.created_at
editor.updated_at
provides:
ScriptSaveRequested(editor.script_id)
ScriptRunRequested(editor.script_id)
ScriptCheckSyntaxRequested(editor.script_id)
ScriptDeleteRequested(editor.script_id)
@guarantee HeaderLayout
-- Header bar with script title tab.
-- Actions (right side): Save button, Run button,
-- Check Syntax button, Delete button (danger style).
@guarantee MetadataRow
-- Two rows of metadata fields above the editor.
-- Row 1: Title (text input), Slug (text input, auto-generated from title).
-- Row 2: Kind (select: utility/macro/transform),
-- Entrypoint (select: dynamically discovered functions with "main" always first),
-- Enabled (checkbox).
@guarantee EditorBody
-- Code editor with Python syntax highlighting.
-- Toolbar: "Content" label.
-- Syntax errors shown as inline markers in editor gutter.
@guarantee FooterLayout
-- Created date and Updated date, locale-formatted.
}
-- ─── Script editor actions ──────────────────────────────────
rule ScriptSave {
when: ScriptSaveRequested(script_id)
-- Lua syntax check first (parse validation)
-- Python syntax check first (parse validation via Python runtime)
-- If syntax error: blocks save, shows error inline in editor gutter
-- If valid: bumps version, saves to DB + rewrites .lua file
-- See engine_side_effects.allium UpdateTemplateSideEffects (same pattern)
-- If valid: bumps version, saves to DB + rewrites .py file
-- Entrypoint list re-discovered from AST after save
}
rule ScriptCheckSyntax {
when: ScriptCheckSyntaxRequested(script_id)
-- Validates Python syntax without saving
-- Shows errors inline in editor gutter
-- Success shown as toast or status indicator
}
rule ScriptRun {
when: ScriptRunRequested(script_id)
-- Executes script in Lua runtime
-- Calls configured entrypoint function (default: render)
-- Executes script in Python runtime
-- Calls configured entrypoint function (default: main)
-- stdout/stderr directed to Output panel tab
-- Output panel auto-opens if not already visible
-- Errors shown in Output panel with line numbers
@@ -45,6 +91,6 @@ rule ScriptRun {
rule ScriptDelete {
when: ScriptDeleteRequested(script_id)
-- No confirmation dialog
-- Deletes DB record + .lua file on disk
-- Deletes DB record + .py file on disk
-- Closes script tab, sidebar removes item
}

View File

@@ -5,116 +5,251 @@
-- Describes the layout and behaviour of the settings and style views.
-- ─── External surfaces ──────────────────────────────────────
surface User {
provides: StyleThemeSelected(theme_name)
provides: StyleApplyRequested(theme_name)
}
use "./i18n.allium" as i18n
-- ─── Settings view ────────────────────────────────────────────
-- VS Code-style settings page with header + search bar + scrollable sections.
-- Search filters sections by keyword match. "No results" with clear button.
value SettingsView {
search_query: String? -- filters sections by keyword match
active_sections: List<String> -- visible sections after search filter
}
-- 8 sections (style is a separate tab, not a settings section):
value SettingsProjectSection {
project_name: String -- text input
project_description: String -- textarea (3 rows)
data_path: String -- text input + Browse button + Reset button
public_url: String -- url input
main_language: String -- select
blog_languages: List<String> -- checkboxes (main language disabled)
default_author: String -- text input
max_posts_per_page: Integer -- number input (1-500, default 50)
blogmark_category: String -- select from categories
}
-- 1. Project Settings:
-- Project Name (text), Project Description (textarea 3 rows),
-- Data Path (text + Browse + Reset), Public URL (url),
-- Main Language (select), Blog Languages (checkboxes, main disabled),
-- Default Author (text), Max Posts Per Page (number 1-500 default 50),
-- Blogmark Category (select), Blogmark Bookmarklet (copy button), Save
value SettingsEditorSection {
default_mode: String -- select: wysiwyg | markdown | preview
diff_view_style: String -- select: inline | side-by-side
wrap_long_lines: Boolean -- checkbox
hide_unchanged_regions: Boolean -- checkbox
}
-- 2. Editor Settings:
-- Default Editor Mode (select: WYSIWYG/Markdown/Preview),
-- Diff View Style (select: Inline/Side-by-side),
-- Wrap Long Lines (checkbox), Hide Unchanged Regions (checkbox)
value SettingsCategoryRow {
name: String -- read-only for protected categories
title: String -- editable
render_in_lists: Boolean -- checkbox
show_titles: Boolean -- checkbox
post_template_slug: String? -- select
list_template_slug: String? -- select
is_protected: Boolean -- true for: article, aside, page, picture
}
-- 3. Content Settings (Categories):
-- Table: Category | Title | Render in Lists | Show Titles | Post Template | List Template | Remove
-- Protected categories: article, aside, page, picture (cannot be deleted)
-- Add Category: text input + Add button. Reset to Defaults button
value SettingsAISection {
anthropic_api_key: String? -- masked + Change/Save
mistral_api_key: String? -- masked + Change/Save
ollama_enabled: Boolean -- toggle, shows model capabilities table (tools/vision)
lm_studio_enabled: Boolean -- toggle, shows model capabilities table
offline_mode: Boolean -- toggle, switches between online and airplane endpoint
default_model: String? -- select from active endpoint models
title_model: String? -- select
image_analysis_model: String? -- select (vision-capable only)
system_prompt: String -- textarea (12 rows) + Save + Reset to Default
}
-- 4. AI Assistant Settings:
-- Two-endpoint configuration (see ai.allium TwoEndpointModel):
-- Online endpoint: URL (text), API Key (masked + Change/Save), Model (select)
-- Airplane endpoint: URL (text), Model (select)
-- Both use OpenAI Chat Completions wire format.
-- Refresh Models button per endpoint: fetches model list from endpoint URL
-- Title Model (select from active endpoint models)
-- Image Analysis Model (select, vision-capable only)
-- Offline mode toggle: switches between online and airplane endpoint
-- Disabled if airplane endpoint URL is not configured
-- System Prompt (textarea 12 rows + Save + Reset to Default)
value SettingsPublishingSection {
ssh_mode: String -- select: scp | rsync
ssh_host: String -- text input
ssh_username: String -- text input
ssh_remote_path: String -- text input
}
-- 5. Technology Settings:
-- Semantic Similarity (checkbox)
value SettingsMCPSection {
status: String -- port number or "Not running"
agents: List<MCPAgentRow>
}
-- 6. Publishing Settings (SSH):
-- Info: SSH key auth only
-- SSH Mode (select: scp/rsync), SSH Host, SSH Username, SSH Remote Path
-- Save + Clear buttons
value MCPAgentRow {
agent_name: String -- Claude Code, Claude Desktop, GitHub Copilot, etc.
is_installed: Boolean -- Add/Remove toggle
}
-- 7. Data Maintenance:
-- Rebuild buttons (6): Posts from Files, Media from Files, Scripts from Files,
-- Templates from Files, Links, Regenerate Missing Thumbnails
-- Open Data Folder button
invariant SettingsProtectedCategories {
-- Protected categories (article, aside, page, picture) cannot be deleted.
-- Their Remove button is disabled.
}
-- 8. MCP Server Settings:
-- Status badge (port or "Not running")
-- Per-agent rows: Claude Code, Claude Desktop, GitHub Copilot, Gemini CLI,
-- OpenCode, Mistral Vibe, OpenAI Codex — each with Add/Remove toggle
invariant SettingsMCPAgents {
-- MCP section has exactly 7 agent rows in this order:
-- Claude Code, Claude Desktop, GitHub Copilot, Gemini CLI,
-- OpenCode, Mistral Vibe, OpenAI Codex.
}
config {
settings_max_posts_per_page: Integer = 500
settings_default_posts_per_page: Integer = 50
settings_system_prompt_rows: Integer = 12
}
surface SettingsViewSurface {
context settings: SettingsView
exposes:
settings.search_query
settings.active_sections
provides:
SettingsSearchChanged(query)
SettingsProjectSaved(project_data)
SettingsEditorSaved(editor_data)
SettingsCategoryAdded(category_name)
SettingsCategoryUpdated(category_row)
SettingsCategoryRemoved(category_name)
SettingsCategoriesResetToDefaults()
SettingsAIApiKeySaved(provider, key)
SettingsAIModelRefreshRequested(endpoint)
SettingsAISystemPromptSaved(prompt)
SettingsAISystemPromptReset()
SettingsPublishingSaved(publishing_data)
SettingsPublishingCleared()
SettingsMCPAgentToggled(agent_name)
SettingsRebuildRequested(entity_type)
SettingsRegenerateThumbnailsRequested()
SettingsOpenDataFolderRequested()
StyleThemeSelected(theme_name)
StyleApplyRequested(theme_name)
@guarantee SearchBar
-- Search input in header filters sections by keyword match.
-- "No results" message with clear button when no sections match.
@guarantee ProjectSection
-- Section 1: Project Name, Description (textarea 3 rows), Data Path (text + Browse + Reset),
-- Public URL, Main Language (select), Blog Languages (checkbox grid, main disabled),
-- Default Author, Max Posts Per Page (number 1-500),
-- Blogmark Category (select), Blogmark Bookmarklet (copy button), Save button.
@guarantee BookmarkletCopy
-- Copy button copies bookmarklet JavaScript to clipboard.
-- Bookmarklet uses project's publicUrl to construct POST endpoint.
@guarantee EditorSection
-- Section 2: Default Editor Mode (select: WYSIWYG/Markdown/Preview),
-- Diff View Style (select: Inline/Side-by-side),
-- Wrap Long Lines (checkbox), Hide Unchanged Regions (checkbox).
@guarantee ContentCategoriesSection
-- Section 3: Table with columns: Category, Title, Render in Lists,
-- Show Titles, Post Template, List Template, Remove.
-- Protected categories (article, aside, page, picture) cannot be deleted.
-- Add Category: text input + Add button, validates non-empty and unique.
-- "Reset to Defaults" restores default categories set.
@guarantee AISection
-- Section 4: Anthropic API key, Mistral API key (both masked + Change/Save),
-- Ollama toggle (with model capabilities table: tools/vision),
-- LM Studio toggle (with capabilities table),
-- Offline mode toggle (with dedicated model selectors for chat/title/image),
-- Default model (with catalog info: max output, context window),
-- Title model, Image analysis model,
-- System prompt (textarea config.settings_system_prompt_rows rows + Save + Reset).
@guarantee AIApiKeyValidation
-- On Save: test API call to endpoint before persisting.
-- On failure: toast error message, key not saved.
-- On success: key encrypted via SecureKeyStore, masked display shown.
@guarantee AIModelRefresh
-- Refresh Models button per endpoint fetches model list from URL.
-- Model selector populated from fetched list.
-- Per-model info: max output tokens, context window (when available).
@guarantee TechnologySection
-- Section 5: Python runtime mode (webworker/main-thread),
-- Semantic Similarity toggle.
@guarantee PublishingSection
-- Section 6: SSH Mode (scp/rsync), Host, Username, Remote Path.
-- Save + Clear buttons.
@guarantee MCPSection
-- Section 7: Status badge (port or "Not running").
-- 7 agent rows with Add/Remove toggle each.
@guarantee DataMaintenanceSection
-- Section 8: 6 rebuild buttons (Posts from Files, Media from Files,
-- Scripts from Files, Templates from Files, Links,
-- Regenerate Missing Thumbnails).
-- Open Data Folder button.
-- Each rebuild executes immediately (no confirmation).
-- Runs as background task with progress in Tasks panel.
@guarantee CollapsibleSections
-- All 8 sections are collapsible.
-- Section visibility respects search filter.
}
-- ─── Settings view actions ──────────────────────────────────
-- API key validation:
-- On Save: test API call to online endpoint before persisting
-- On validation failure: toast error message, key not saved
-- On success: key encrypted via SecureKeyStore, masked display shown
-- Endpoint configuration:
-- URL field required for both endpoints
-- Refresh Models button fetches model list from endpoint URL
-- Model selector populated from fetched model list
-- Per-model info: max output tokens, context window (when available)
-- Rebuild operations (Data Maintenance section):
-- 6 buttons, each executes immediately (no confirmation dialog)
-- Runs as background task with progress visible in Tasks panel
-- On completion: wholesale replaces store data for that entity type
-- Sidebar and editors re-render with fresh data
-- Category management (Content Settings):
-- Protected categories: article, aside, page, picture (Remove button disabled)
-- Add Category: text input + Add button, validates non-empty and unique
-- Per-category settings: editable inline in table row
-- "Reset to Defaults" restores default categories set
-- MCP per-agent config:
-- Add/Remove toggle per agent row
-- Status badge: shows port number when running, "Not running" otherwise
-- Bookmarklet (Project Settings):
-- Copy button copies bookmarklet JavaScript to clipboard
-- Bookmarklet uses project's publicUrl to construct POST endpoint
rule SettingsRebuild {
when: SettingsRebuildRequested(entity_type)
-- entity_type: posts | media | scripts | templates | links | thumbnails
-- Executes immediately (no confirmation dialog)
-- Runs as background task with progress visible in Tasks panel
-- On completion: wholesale replaces store data for that entity type
-- Sidebar and editors re-render with fresh data
}
-- ─── Style view ───────────────────────────────────────────────
-- Simple vertical layout: header, theme picker, controls row, preview.
value StyleView {
themes: List<StyleTheme>
selected_theme: String?
applied_theme: String?
preview_mode: String -- auto | light | dark
}
-- Theme Picker: grid of theme buttons (one per Pico CSS theme).
-- Each button: swatch with 3 colour tones (accent, light bg, dark bg), theme name.
-- Selected theme: highlighted with aria-pressed.
value StyleTheme {
name: String
accent_color: String -- hex
light_bg_color: String -- hex
dark_bg_color: String -- hex
}
-- Controls row: Preview Mode dropdown (Auto/Light/Dark), Apply Theme button.
surface StyleViewSurface {
context style: StyleView
-- Preview: iframe of style preview page at 127.0.0.1:4123/__style-preview
-- with theme and mode query params. Updates live on selection change.
exposes:
for t in style.themes:
t.name
t.accent_color
t.light_bg_color
t.dark_bg_color
style.selected_theme
style.applied_theme
style.preview_mode
-- Apply Theme persists to project metadata.
provides:
StyleThemeSelected(theme_name)
StyleApplyRequested(style.selected_theme)
when style.selected_theme != style.applied_theme
StylePreviewModeChanged(mode)
-- ─── Style view actions ─────────────────────────────────────
@guarantee ThemePicker
-- Grid of theme buttons (one per Pico CSS theme).
-- Each button: swatch with 3 colour tones (accent, light bg, dark bg) + theme name.
-- Selected theme highlighted with aria-pressed.
@guarantee ControlsRow
-- Preview Mode dropdown (Auto/Light/Dark).
-- Apply Theme button, disabled when selected_theme matches applied_theme.
@guarantee LivePreview
-- Iframe at 127.0.0.1:4123/__style-preview with theme and mode query params.
-- Updates live on selection or preview mode change.
@guarantee PreviewModeLocal
-- Preview mode dropdown controls iframe query param only.
-- Does not persist — local UI state only.
}
rule StyleThemeSelect {
when: StyleThemeSelected(theme_name)
@@ -127,7 +262,3 @@ rule StyleApplyTheme {
-- Persists picoTheme to project metadata (meta/project.json)
-- See engine_side_effects.allium UpdateProjectMetadataSideEffects
}
-- Preview mode dropdown (Auto/Light/Dark):
-- Controls iframe query param, live update on change
-- Does not persist — local UI state only

View File

@@ -5,61 +5,105 @@
-- Describes the layout and behaviour of the tags view.
use "./tag.allium" as tag
use "./i18n.allium" as i18n
-- ─── Tags view ────────────────────────────────────────────────
-- Three sections controlled by sidebar TagsNav:
value TagsView {
cloud_tags: List<TagCloudItem>
selected_tags: List<String> -- multi-select from cloud
edit_form: TagEditForm? -- populated when exactly 1 tag selected
merge_source_tags: List<String> -- 2+ selected tags for merge
merge_target: String? -- target tag for merge
}
-- 1. Cloud: tag cloud visualization. Tags sized by post count,
-- coloured by tag colour. Clickable to select for editing.
value TagCloudItem {
name: String
count: Integer
color: String?
}
-- 2. Manage (Create/Edit): tag name input, colour picker, template select.
-- Edit existing tag or create new. Delete button with confirmation.
-- 3. Merge: source tag select, target tag select, Merge button.
-- Merges all posts from source tag into target, removes source.
-- ─── Tags view actions ──────────────────────────────────────
-- Cloud section:
-- Tags sized by post count (same 11-22px scaling as dashboard)
-- Tags with colours get coloured background
-- Click selects tag: switches to Manage section, loads tag data
-- Manage section:
-- Create new: empty form, Save creates tag + writes tags.json
-- Edit existing: loads tag data into form, Save updates
-- Tag name input, colour picker, template select (optional)
-- Colour picker: 17 preset colour swatches + custom hex input field
-- Delete: opens ConfirmDialog showing post count
-- "This tag is used in N posts. Delete anyway?"
-- On confirm: background task removes tag from all posts,
-- rewrites published .md files, deletes tag, writes tags.json
-- Merge section:
-- Source tag multi-select, target tag single-select
-- Merge button: opens ConfirmDialog
-- "Merge N tags into {target}? This cannot be undone."
-- On confirm: background task updates all affected posts,
-- rewrites published .md files, deletes source tags, writes tags.json
-- Sync (maintenance action):
-- Rewrites tags.json from current DB state
-- ─── Colour picker popover ──────────────────────────────────
value TagEditForm {
name: String -- editable text input
color: String? -- colour picker value
post_template_slug: String? -- select from available templates
}
value ColourPickerPopover {
-- Inline popover positioned below tag colour field (tags view manage).
-- Not a modal — does not dim backdrop, no ESC/backdrop close.
-- Closes when clicking outside popover or selecting a colour.
-- Grid of 17 preset colour swatches (4x4 + 1).
-- Below grid: custom hex input field (#RRGGBB).
-- Selection is immediate — no confirm/cancel buttons.
-- Choosing a colour updates the tag form's colour field live.
presets: List<String> -- 17 hex colour values
presets: List<String> -- 17 hex colour values
custom_hex: String?
selected: String?
}
config {
colour_picker_preset_count: Integer = 17
tag_cloud_min_font: String = "0.85rem"
tag_cloud_max_font: String = "1.8rem"
}
surface TagsViewSurface {
context view: TagsView
exposes:
for t in view.cloud_tags:
t.name
t.count
t.color
view.selected_tags
view.edit_form when view.edit_form != null
view.merge_target when view.merge_target != null
provides:
TagCloudItemClicked(tag_name)
TagCreateRequested(name, color)
TagSaveRequested(name, color, post_template_slug)
when view.edit_form != null
TagDeleteRequested(tag_name)
when view.selected_tags.count = 1
TagMergeRequested(source_tags, target_tag)
when view.selected_tags.count >= 2
TagSyncRequested()
@guarantee CloudSection
-- Tag cloud visualisation (section #1).
-- Tags sized by post count (config.tag_cloud_min_font to config.tag_cloud_max_font).
-- Tags with colours get coloured background with contrast text.
-- Multi-select: click to toggle selection. Selection count shown with clear button.
-- Click selects tag for editing in Manage section.
@guarantee ManageSection
-- Create/Edit section (section #2).
-- Create form: name input + colour picker + Create button.
-- Edit form (when exactly 1 tag selected): name input, colour picker,
-- post template select dropdown (optional), Save/Cancel buttons.
-- Delete button visible when exactly 1 tag selected.
@guarantee ColourPicker
-- Inline popover positioned below tag colour field.
-- Grid of config.colour_picker_preset_count preset colour swatches.
-- Below grid: custom hex input field (#RRGGBB).
-- Selection is immediate — no confirm/cancel buttons.
-- Choosing a colour updates the form's colour field live.
@guarantee MergeSection
-- Merge section (section #3). Requires 2+ selected tags.
-- Target tag select dropdown (single select from selected tags).
-- Merge button opens ConfirmDialog:
-- "Merge N tags into {target}? This cannot be undone."
-- Shows preview of "tags to delete" (all selected except target).
@guarantee SyncSection
-- Sync/Discover section (section #4).
-- "Discover" button rewrites tags.json from current DB state.
@guarantee DeleteConfirmation
-- Delete opens ConfirmDialog showing post count:
-- "This tag is used in N posts. Delete anyway?"
-- On confirm: background task removes tag from all posts,
-- rewrites published .md files, deletes tag, writes tags.json.
@guarantee MergeExecution
-- On merge confirm: background task updates all affected posts,
-- rewrites published .md files, deletes source tags, writes tags.json.
}

View File

@@ -6,22 +6,56 @@
-- Describes the layout and behaviour of the template editor.
-- See template.allium for entity model and Liquid subset.
-- ─── External surfaces ──────────────────────────────────────
surface User {
provides: TemplateSaveRequested(template_id)
provides: TemplateDeleteRequested(template_id)
}
use "./template.allium" as template
use "./i18n.allium" as i18n
-- ─── Template editor view ─────────────────────────────────────
-- Code editor for Liquid templates (post, list, not_found, partial).
value TemplateEditorView {
template_id: String
title: String -- editable text input
slug: String -- editable text input, auto-generated from title
kind: String -- select: post | list | not_found | partial
enabled: Boolean -- checkbox
content: String -- code editor content
created_at: String -- locale-formatted date
updated_at: String -- locale-formatted date
}
-- Layout: header bar, code editor, preview pane.
-- Header: template title (editable), kind badge, enabled toggle, version display.
-- Editor: code editor with Liquid syntax highlighting.
-- Preview: rendered template output in iframe (uses sample post data).
-- Toolbar: Save button, Delete button.
surface TemplateEditorSurface {
context editor: TemplateEditorView
exposes:
editor.title
editor.slug
editor.kind
editor.enabled
editor.created_at
editor.updated_at
provides:
TemplateSaveRequested(editor.template_id)
TemplateValidateRequested(editor.template_id)
TemplateDeleteRequested(editor.template_id)
@guarantee HeaderLayout
-- Header bar with template title tab.
-- Actions (right side): Save button, Validate button,
-- Delete button (danger style).
@guarantee MetadataRow
-- Two rows of metadata fields above the editor.
-- Row 1: Title (text input), Slug (text input, auto-generated from title).
-- Row 2: Kind (select: post/list/not-found/partial), Enabled (checkbox).
@guarantee EditorBody
-- Code editor with HTML/Liquid syntax highlighting.
-- Toolbar: "Content" label.
-- Syntax errors shown inline in editor on validation.
@guarantee FooterLayout
-- Created date and Updated date, locale-formatted.
}
-- ─── Template editor actions ────────────────────────────────
@@ -33,6 +67,13 @@ rule TemplateSave {
-- See engine_side_effects.allium UpdateTemplateSideEffects
}
rule TemplateValidate {
when: TemplateValidateRequested(template_id)
-- Validates Liquid syntax without saving
-- Shows errors inline in editor
-- Success shown as toast or status indicator
}
rule TemplateDelete {
when: TemplateDeleteRequested(template_id)
-- Checks for references: posts using this template, tags with postTemplateSlug