chore: and more cleanups

This commit is contained in:
2026-04-09 17:46:34 +02:00
parent b7807161f0
commit ee961f1b02
15 changed files with 529 additions and 16 deletions

View File

@@ -19,6 +19,16 @@ entity AiEndpoint {
-- airplane: local model (Ollama, LM Studio, etc.)
}
surface AiEndpointSurface {
context endpoint: AiEndpoint
exposes:
endpoint.kind
endpoint.url
endpoint.api_key when endpoint.api_key != null
endpoint.model
}
entity SecureKeyStore {
-- Encrypts API keys using OS keychain
-- macOS: Keychain, Windows: DPAPI, Linux: libsecret
@@ -26,6 +36,10 @@ entity SecureKeyStore {
-- No plain-text fallback
}
surface SecureKeyStoreSurface {
context _: SecureKeyStore
}
entity ChatConversation {
title: String
model: String
@@ -35,6 +49,17 @@ entity ChatConversation {
messages: ChatMessage with conversation = this
}
surface ChatConversationSurface {
context conversation: ChatConversation
exposes:
conversation.title
conversation.model
conversation.created_at
conversation.updated_at
conversation.messages.count
}
entity ChatMessage {
conversation: ChatConversation
role: system | user | assistant | tool
@@ -44,6 +69,18 @@ entity ChatMessage {
created_at: Timestamp
}
surface ChatMessageSurface {
context message: ChatMessage
exposes:
message.conversation
message.role
message.content
message.token_usage_input when message.token_usage_input != null
message.token_usage_output when message.token_usage_output != null
message.created_at
}
surface OneShotAiSurface {
facing _: AiOperator

View File

@@ -17,6 +17,13 @@ value GitSyncStatus {
kind: local_only | remote_only | both
}
surface GitSyncStatusSurface {
context status: GitSyncStatus
exposes:
status.kind
}
entity GitRepository {
is_initialized: Boolean
remote_url: String?
@@ -25,6 +32,17 @@ entity GitRepository {
has_lfs: Boolean
}
surface GitRepositorySurface {
context repo: GitRepository
exposes:
repo.is_initialized
repo.remote_url when repo.remote_url != null
repo.provider when repo.provider != null
repo.current_branch when repo.current_branch != null
repo.has_lfs
}
surface GitControlSurface {
facing _: GitOperator

View File

@@ -11,6 +11,24 @@
use "./i18n.allium" as i18n
use "./task.allium" as task
surface LayoutControlSurface {
facing _: LayoutOperator
provides:
ToggleSidebarRequested()
TogglePanelRequested()
ToggleAssistantSidebarRequested()
ActivityClicked(activity_id)
}
surface LayoutRuntimeSurface {
facing _: LayoutRuntime
provides:
GitBadgePollTick(badge)
ClearGitBadgeTick(badge)
}
-- ─── Window shell ─────────────────────────────────────────────
-- +------------------------------------------------------------+
@@ -37,6 +55,17 @@ value AppShell {
status_bar: StatusBar
}
surface AppShellSurface {
context shell: AppShell
exposes:
shell.title_bar.title
shell.sidebar.visible
shell.sidebar.width
shell.content_area.panel.visible
shell.assistant_sidebar.visible
}
value ContentArea {
-- tab_bar: see tabs.allium
-- editor: routed by active tab; see tabs.allium
@@ -69,6 +98,15 @@ value ShellVisibility {
assistant_sidebar_visible: Boolean
}
surface ShellVisibilitySurface {
context visibility: ShellVisibility
exposes:
visibility.sidebar_visible
visibility.panel_visible
visibility.assistant_sidebar_visible
}
rule ToggleSidebar {
when: ToggleSidebarRequested()
ensures: sidebar_visible = not sidebar_visible

View File

@@ -15,6 +15,16 @@ entity McpServer {
is_running: Boolean
}
surface McpServerSurface {
context server: McpServer
exposes:
server.transport
server.host
server.port
server.is_running
}
entity Proposal {
kind: draft_post | propose_script | propose_template | propose_media_metadata | propose_post_metadata
status: pending | accepted | discarded | expired
@@ -38,6 +48,24 @@ entity Proposal {
}
}
surface ProposalSurface {
context proposal: Proposal
exposes:
proposal.kind
proposal.status
proposal.entity_id
proposal.data
proposal.created_at
proposal.expires_at
proposal.draft_post when proposal.draft_post != null
proposal.proposed_script when proposal.proposed_script != null
proposal.proposed_template when proposal.proposed_template != null
proposal.target_media when proposal.target_media != null
proposal.target_post when proposal.target_post != null
proposal.is_expired
}
config {
http_port: Integer = 4124
proposal_ttl_app: Duration = 30.minutes
@@ -339,6 +367,13 @@ value McpAgentKind {
kind: String
}
surface McpAgentKindSurface {
context agent_kind: McpAgentKind
exposes:
agent_kind.kind
}
rule InstallAgentConfig {
when: InstallAgentConfigRequested(agent_kind)
-- Writes stdio MCP server config into the agent's config file

View File

@@ -5,6 +5,17 @@
use "./project.allium" as project
surface MediaControlSurface {
facing _: MediaOperator
provides:
ImportMediaRequested(project, source_file)
UpdateMediaRequested(media, changes)
DeleteMediaRequested(media)
UpsertMediaTranslationRequested(media, language, title, alt, caption)
RebuildMediaFromFilesRequested(project)
}
value ThumbnailSet {
small: String -- 150px width (binary path)
medium: String -- 400px width (binary path)
@@ -19,6 +30,13 @@ value SidecarFile {
path: String
}
surface SidecarFileSurface {
context sidecar: SidecarFile
exposes:
sidecar.path
}
entity Media {
project: project/Project
filename: String
@@ -48,6 +66,37 @@ entity Media {
thumbnails: ThumbnailSet
}
surface MediaSurface {
context media: Media
exposes:
media.project
media.filename
media.original_name
media.mime_type
media.size
media.width when media.width != null
media.height when media.height != null
media.title when media.title != null
media.alt when media.alt != null
media.caption when media.caption != null
media.author when media.author != null
media.language when media.language != null
media.file_path
media.sidecar_path
media.checksum when media.checksum != null
media.tags
media.created_at
media.updated_at
media.translations.count
media.linked_posts.count
media.available_languages
media.thumbnails.small
media.thumbnails.medium
media.thumbnails.large
media.thumbnails.ai
}
entity MediaTranslation {
media: Media
language: String

View File

@@ -10,6 +10,23 @@
use "./media.allium" as media
use "./search.allium" as search
surface MediaProcessingControlSurface {
facing _: MediaProcessingOperator
provides:
ImportMediaRequested(source_path, project)
TagMediaRequested(media, tags)
DeleteMediaRequested(media)
ValidateMediaRequested(project)
}
surface MediaProcessingRuntimeSurface {
facing _: MediaProcessingRuntime
provides:
MediaImported(media)
}
-- ============================================================================
-- MEDIA FILE ORGANIZATION
-- ============================================================================
@@ -27,6 +44,18 @@ value MediaFileLayout {
thumbnail_ai: String -- thumbnails/{prefix}/{id}-ai.jpg
}
surface MediaFileLayoutSurface {
context layout: MediaFileLayout
exposes:
layout.binary_path
layout.sidecar_path
layout.thumbnail_small
layout.thumbnail_medium
layout.thumbnail_large
layout.thumbnail_ai
}
invariant MediaFileNaming {
-- Original filename is preserved in original_name field
-- Stored filename uses UUID v4: {uuid}.{ext}
@@ -109,6 +138,10 @@ value ThumbnailGeneration {
-- No _source copy is made. Thumbnails regenerated from the original binary.
}
surface ThumbnailGenerationSurface {
context _: ThumbnailGeneration
}
invariant ThumbnailExifHandling {
-- EXIF orientation IS applied during thumbnail generation so that
-- thumbnails always appear right-side-up regardless of camera metadata.
@@ -140,6 +173,14 @@ value ImageProcessing {
-- 4. EXIF data is not stripped (thumbnails are re-encoded, so EXIF is naturally absent)
}
surface ImageProcessingSurface {
context processing: ImageProcessing
exposes:
processing.input_formats
processing.output_formats
}
rule ProcessImageMetadata {
when: MediaImported(media)
-- Extract image metadata from raw file header
@@ -168,6 +209,17 @@ value MediaTranslationFile {
caption: String?
}
surface MediaTranslationFileSurface {
context file: MediaTranslationFile
exposes:
file.translation_for
file.language
file.title when file.title != null
file.alt when file.alt != null
file.caption when file.caption != null
}
invariant MediaTranslationFileLayout {
for t in MediaTranslations:
-- Translation sidecars sit next to the binary, with language suffix

View File

@@ -3,6 +3,13 @@
-- Scope: core (read for rendering), extension Bucket F (menu editor UI)
-- Distilled from: src/main/engine/MenuEngine.ts
surface MenuManagementSurface {
facing _: MenuOperator
provides:
UpdateMenuRequested(menu, items)
}
value MenuItem {
kind: page | submenu | category_archive | home
label: String
@@ -18,6 +25,15 @@ entity Menu {
home_entry: home_items.first
}
surface MenuSurface {
context menu: Menu
exposes:
menu.items.count
menu.home_items.count
menu.home_entry.label
}
invariant HomeAlwaysPresent {
-- The menu always has a Home entry, extracted and prepended
for menu in Menus:

View File

@@ -25,6 +25,13 @@ value AISuggestionsModal {
-- Cancel discards all; Apply writes only accepted fields to entity
}
surface AISuggestionsModalSurface {
context modal: AISuggestionsModal
exposes:
modal.fields.count
}
value AISuggestionField {
label: String
current_value: String
@@ -65,6 +72,16 @@ value InsertPostLinkModal {
related_posts: List<InsertLinkResult> -- similarity-based, shown when query empty
}
surface InsertPostLinkModalSurface {
context modal: InsertPostLinkModal
exposes:
modal.active_tab
modal.search_query
modal.results.count
modal.related_posts.count
}
value InsertLinkResult {
post_id: String
title: String
@@ -87,6 +104,14 @@ value InsertMediaModal {
results: List<InsertMediaResult>
}
surface InsertMediaModalSurface {
context modal: InsertMediaModal
exposes:
modal.search_query
modal.results.count
}
value InsertMediaResult {
media_id: String
title: String
@@ -108,6 +133,14 @@ value LanguagePickerModal {
available_targets: List<LanguageTarget>
}
surface LanguagePickerModalSurface {
context modal: LanguagePickerModal
exposes:
modal.source_language
modal.available_targets.count
}
value LanguageTarget {
code: String
name: String
@@ -130,6 +163,16 @@ value ConfirmDeleteModal {
reference_list: List<String> -- titles of referencing entities
}
surface ConfirmDeleteModalSurface {
context modal: ConfirmDeleteModal
exposes:
modal.entity_name
modal.entity_type
modal.reference_count
modal.reference_list
}
-- ─── Confirm Dialog ──────────────────────────────────────────
value ConfirmDialog {
@@ -141,6 +184,14 @@ value ConfirmDialog {
message: String
}
surface ConfirmDialogSurface {
context modal: ConfirmDialog
exposes:
modal.title
modal.message
}
-- Native confirm dialogs (via rfd crate) are NOT modelled as values.
-- They are simple yes/no system dialogs with a message string.
-- Used by: PostDelete, PostDiscard, TemplateDelete (with references).
@@ -157,6 +208,14 @@ value GalleryOverlay {
images: List<GalleryImage>
}
surface GalleryOverlaySurface {
context overlay: GalleryOverlay
exposes:
overlay.post_id
overlay.images.count
}
value GalleryImage {
media_id: String
thumbnail_url: String -- bds-thumb://media_id
@@ -176,6 +235,17 @@ value LightboxView {
alt_text: String?
}
surface LightboxViewSurface {
context view: LightboxView
exposes:
view.current_index
view.total_count
view.media_id
view.image_url
view.alt_text when view.alt_text != null
}
-- All modals rendered as centered overlay with backdrop dimming.
-- ESC key or backdrop click closes modal (cancel semantics).
-- Overlays (PostPicker, ColourPicker) are positioned inline near trigger.

View File

@@ -3,6 +3,15 @@
-- Scope: core (Wave 1)
-- Distilled from: src/main/engine/ProjectEngine.ts, schema.ts
surface ProjectControlSurface {
facing _: ProjectOperator
provides:
CreateProjectRequested(name, data_path)
SetActiveProjectRequested(project)
DeleteProjectRequested(project)
}
entity Project {
name: String
slug: String
@@ -25,6 +34,24 @@ entity Project {
-- Custom data path overrides default
}
surface ProjectSurface {
context project: Project
exposes:
project.name
project.slug
project.description when project.description != null
project.data_path when project.data_path != null
project.is_active
project.created_at
project.updated_at
project.posts.count
project.media.count
project.tags.count
project.internal_base_dir
project.effective_data_dir
}
invariant SingleActiveProject {
-- Exactly one project is active at any time
let active = Projects where is_active

View File

@@ -25,6 +25,26 @@ value UploadTarget {
remote_dir: String
}
surface PublishJobSurface {
context job: PublishJob
exposes:
job.ssh_host
job.ssh_user
job.ssh_remote_path
job.ssh_mode
job.status
}
surface UploadTargetSurface {
context target: UploadTarget
exposes:
target.kind
target.local_dir
target.remote_dir
}
surface PublishingControlSurface {
facing _: PublishOperator

View File

@@ -26,6 +26,24 @@ entity Script {
}
}
surface ScriptSurface {
context script: Script
exposes:
script.slug
script.title
script.kind
script.entrypoint
script.enabled
script.status
script.content when script.content != null
script.version
script.file_path
script.created_at
script.updated_at
script.content_location
}
surface ScriptManagementSurface {
facing _: ScriptOperator

View File

@@ -7,6 +7,22 @@
use "./post.allium" as post
use "./media.allium" as media
surface SearchControlSurface {
facing _: SearchOperator
provides:
SearchPostsRequested(query, filters)
SearchMediaRequested(query)
}
surface SearchIndexRuntimeSurface {
facing _: SearchRuntime
provides:
SearchIndexUpdated(post)
SearchIndexUpdated(media)
}
value StemmerLanguage {
-- Snowball stemmers for 24 languages
-- ISO 639-1 to Snowball mapping
@@ -14,6 +30,13 @@ value StemmerLanguage {
code: String
}
surface StemmerLanguageSurface {
context language: StemmerLanguage
exposes:
language.code
}
entity PostSearchIndex {
-- SQLite FTS5 virtual table
-- Indexed fields: title, excerpt, content, tags, categories

View File

@@ -6,6 +6,18 @@
use "./project.allium" as project
use "./post.allium" as post
surface TagControlSurface {
facing _: TagOperator
provides:
CreateTagRequested(project, name, color)
UpdateTagRequested(tag, changes)
DeleteTagRequested(tag)
RenameTagRequested(tag, new_name)
MergeTagsRequested(sources, target)
SyncTagsFromPostsRequested(project)
}
entity Tag {
project: project/Project
name: String
@@ -19,6 +31,20 @@ entity Tag {
post_count: posts.count
}
surface TagSurface {
context tag: Tag
exposes:
tag.project
tag.name
tag.color when tag.color != null
tag.post_template_slug when tag.post_template_slug != null
tag.created_at
tag.updated_at
tag.posts.count
tag.post_count
}
invariant UniqueTagNamePerProject {
-- Case-insensitive uniqueness
for a in Tags:

View File

@@ -20,6 +20,37 @@ entity Task {
}
}
surface TaskControlSurface {
facing _: TaskOperator
provides:
SubmitTaskRequested(name, work)
CancelTaskRequested(task)
RegisterExternalTaskRequested(name)
}
surface TaskRuntimeSurface {
facing _: TaskRuntime
provides:
TaskWorkCompleted(task)
TaskWorkFailed(task, error_message)
ProgressReported(task, value, message)
}
surface TaskSurface {
context task: Task
exposes:
task.name
task.status
task.progress when task.progress != null
task.message when task.message != null
task.group_id when task.group_id != null
task.group_name when task.group_name != null
task.created_at
}
config {
max_concurrent: Integer = 3
progress_throttle: Duration = 250.milliseconds
@@ -39,9 +70,11 @@ invariant FifoQueue {
rule SubmitTask {
when: SubmitTaskRequested(name, work)
let running_tasks = Tasks where status = running
ensures: Task.created(name: name, status: pending)
ensures:
let task = Task.created(name: name, status: pending)
task.status = pending
if running_tasks.count < config.max_concurrent:
task.status = running
TaskStarted(task, work)
}
@@ -82,7 +115,9 @@ invariant ProgressThrottled {
-- External tasks: lifecycle controlled by caller (e.g., renderer-side scripts)
rule RegisterExternalTask {
when: RegisterExternalTaskRequested(name)
ensures: Task.created(name: name, status: running)
ensures:
let task = Task.created(name: name, status: running)
task.status = running
@guidance
-- External tasks are not managed by the queue
-- The caller is responsible for updating status

View File

@@ -7,11 +7,35 @@
use "./post.allium" as post
use "./media.allium" as media
surface TranslationControlSurface {
facing _: TranslationOperator
provides:
UpsertPostTranslationRequested(post, language, title, content, excerpt)
DeletePostTranslationRequested(translation)
ValidateTranslationsRequested(project)
}
surface TranslationRuntimeSurface {
facing _: TranslationRuntime
provides:
PostPublished(post)
PublishTranslationRequested(translation)
}
value SupportedLanguage {
-- en, de, fr, it, es
code: String
}
surface SupportedLanguageSurface {
context language: SupportedLanguage
exposes:
language.code
}
entity PostTranslation {
canonical_post: post/Post
language: SupportedLanguage
@@ -34,6 +58,24 @@ entity PostTranslation {
}
}
surface PostTranslationSurface {
context translation: PostTranslation
exposes:
translation.canonical_post
translation.language.code
translation.title
translation.excerpt when translation.excerpt != null
translation.content when translation.content != null
translation.status
translation.file_path
translation.checksum when translation.checksum != null
translation.created_at
translation.updated_at
translation.published_at when translation.published_at != null
translation.content_location
}
invariant UniqueTranslationPerLanguage {
for a in PostTranslations:
for b in PostTranslations:
@@ -54,15 +96,17 @@ invariant TranslationFilePath {
rule UpsertPostTranslation {
when: UpsertPostTranslationRequested(post, language, title, content, excerpt)
requires: not post.do_not_translate
ensures: PostTranslation.created(
canonical_post: post,
language: language,
title: title,
content: content,
excerpt: excerpt,
status: draft,
file_path: ""
)
ensures:
let translation = PostTranslation.created(
canonical_post: post,
language: language,
title: title,
content: content,
excerpt: excerpt,
status: draft,
file_path: ""
)
translation.status = draft
-- If translation already exists, update it instead
}
@@ -70,11 +114,16 @@ rule PublishPostTranslation {
when: PostPublished(post)
-- All translations are also published when the canonical post is published
for t in post.translations:
ensures: t.status = published
ensures: t.published_at = t.published_at ?? now
ensures: TranslationFileWritten(t)
ensures: t.content = null
-- Content moves to filesystem
ensures: PublishTranslationRequested(t)
}
rule PublishTranslation {
when: PublishTranslationRequested(translation)
ensures: translation.status = published
ensures: translation.published_at = translation.published_at ?? now
ensures: TranslationFileWritten(translation)
ensures: translation.content = null
-- Content moves to filesystem
}
rule DeletePostTranslation {