chore: and more cleanups
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user