B1-5..B1-20: distill remaining code behaviors into specs (rendering.allium, post/media/task/generation/editor specs)
This commit is contained in:
@@ -20,6 +20,7 @@ use "./metadata.allium" as metadata -- Project config, categories, publi
|
||||
|
||||
-- Infrastructure
|
||||
use "./search.allium" as search -- FTS5 full-text search with Snowball stemming
|
||||
use "./rendering.allium" as rendering -- Template render assigns, filters, macros (preview + generation)
|
||||
use "./generation.allium" as generation -- Static site generation (sections, routes, hashing)
|
||||
use "./preview.allium" as preview -- Local HTTP preview server
|
||||
use "./publishing.allium" as publishing -- SSH upload (SCP / rsync)
|
||||
|
||||
@@ -130,9 +130,15 @@ surface MediaEditorSurface {
|
||||
@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.
|
||||
-- Per-translation actions: click to edit (opens modal), refresh button, delete button.
|
||||
-- "No translations" message when list is empty.
|
||||
|
||||
@guarantee TranslationEditModal
|
||||
-- Editing a translation opens a modal dialog ("Edit Translation"), not
|
||||
-- an inline form. Hidden language field plus Title, Alt Text, and
|
||||
-- Caption (textarea) inputs. Footer: Cancel and Save buttons.
|
||||
-- Save persists the translation; Cancel/close discards.
|
||||
|
||||
@guarantee LinkedPostsSection
|
||||
-- "Link to Post" button opens inline post picker overlay.
|
||||
-- List of currently linked posts with document icon. Click navigates to post tab.
|
||||
@@ -214,8 +220,9 @@ rule MediaLinkToPost {
|
||||
|
||||
rule MediaTranslationEdit {
|
||||
when: MediaTranslationEditClicked(media_id, language)
|
||||
-- Loads translation fields inline (title, alt, caption) for that language
|
||||
-- Edit in place, save persists to translated sidecar {path}.{lang}.meta
|
||||
-- Opens the "Edit Translation" modal pre-filled with the translation's
|
||||
-- title, alt, and caption for that language
|
||||
-- Save persists to DB + translated sidecar {path}.{lang}.meta; Cancel discards
|
||||
}
|
||||
|
||||
rule MediaTranslationRefresh {
|
||||
|
||||
@@ -19,6 +19,7 @@ value ScriptEditorView {
|
||||
entrypoint: String -- select: discovered Lua functions available as entrypoints
|
||||
enabled: Boolean -- checkbox
|
||||
content: String -- code editor content
|
||||
can_publish: Boolean -- true while status = draft
|
||||
created_at: String -- locale-formatted date
|
||||
updated_at: String -- locale-formatted date
|
||||
}
|
||||
@@ -37,13 +38,16 @@ surface ScriptEditorSurface {
|
||||
|
||||
provides:
|
||||
ScriptSaveRequested(editor.script_id)
|
||||
ScriptPublishRequested(editor.script_id)
|
||||
when editor.can_publish
|
||||
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,
|
||||
-- Actions (right side): Save button, Publish button (shown only when
|
||||
-- can_publish, i.e. status = draft), Run button,
|
||||
-- Check Syntax button, Delete button (danger style).
|
||||
|
||||
@guarantee MetadataRow
|
||||
@@ -72,6 +76,15 @@ rule ScriptSave {
|
||||
-- Entrypoint list re-discovered from Lua source after save
|
||||
}
|
||||
|
||||
rule ScriptPublish {
|
||||
when: ScriptPublishRequested(script_id)
|
||||
-- Only offered while the script is a draft (can_publish gate on the button)
|
||||
-- Validates Lua syntax first (publish gate); invalid source blocks publish
|
||||
-- Saves current draft fields, then publishes (status -> published)
|
||||
-- Writes the published script file to disk
|
||||
-- See script.allium PublishScript
|
||||
}
|
||||
|
||||
rule ScriptCheckSyntax {
|
||||
when: ScriptCheckSyntaxRequested(script_id)
|
||||
-- Validates Lua syntax without saving
|
||||
|
||||
@@ -18,6 +18,7 @@ value TemplateEditorView {
|
||||
kind: String -- select: post | list | not_found | partial
|
||||
enabled: Boolean -- checkbox
|
||||
content: String -- code editor content
|
||||
can_publish: Boolean -- true while status = draft
|
||||
created_at: String -- locale-formatted date
|
||||
updated_at: String -- locale-formatted date
|
||||
}
|
||||
@@ -35,12 +36,15 @@ surface TemplateEditorSurface {
|
||||
|
||||
provides:
|
||||
TemplateSaveRequested(editor.template_id)
|
||||
TemplatePublishRequested(editor.template_id)
|
||||
when editor.can_publish
|
||||
TemplateValidateRequested(editor.template_id)
|
||||
TemplateDeleteRequested(editor.template_id)
|
||||
|
||||
@guarantee HeaderLayout
|
||||
-- Header bar with template title tab.
|
||||
-- Actions (right side): Save button, Validate button,
|
||||
-- Actions (right side): Save button, Publish button (shown only when
|
||||
-- can_publish, i.e. status = draft), Validate button,
|
||||
-- Delete button (danger style).
|
||||
|
||||
@guarantee MetadataRow
|
||||
@@ -67,6 +71,15 @@ rule TemplateSave {
|
||||
-- See engine_side_effects.allium UpdateTemplateSideEffects
|
||||
}
|
||||
|
||||
rule TemplatePublish {
|
||||
when: TemplatePublishRequested(template_id)
|
||||
-- Only offered while the template is a draft (can_publish gate on the button)
|
||||
-- Validates Liquid first (publish gate); invalid source blocks publish
|
||||
-- Saves current draft fields, then publishes (status -> published)
|
||||
-- Writes the published .liquid file to disk
|
||||
-- See template.allium PublishTemplate
|
||||
}
|
||||
|
||||
rule TemplateValidate {
|
||||
when: TemplateValidateRequested(template_id)
|
||||
-- Validates Liquid syntax without saving
|
||||
|
||||
@@ -121,10 +121,13 @@ rule GenerateCoreSectionPages {
|
||||
-- Atom feed
|
||||
ensures: FileGenerated("calendar.json")
|
||||
-- Post dates for calendar widget
|
||||
ensures: FileGenerated("404.html")
|
||||
-- Not-found page rendered from the not-found template
|
||||
for lang in generation.blog_languages - {generation.language}:
|
||||
ensures: FileGenerated(format("{lang}/index.html", lang: lang))
|
||||
ensures: FileGenerated(format("{lang}/feed.xml", lang: lang))
|
||||
ensures: FileGenerated(format("{lang}/atom.xml", lang: lang))
|
||||
ensures: FileGenerated(format("{lang}/404.html", lang: lang))
|
||||
}
|
||||
|
||||
-- Single section: one HTML page per published post
|
||||
|
||||
@@ -11,6 +11,7 @@ surface MediaControlSurface {
|
||||
provides:
|
||||
ImportMediaRequested(source_path, project, metadata)
|
||||
UpdateMediaRequested(media, changes)
|
||||
ReplaceMediaFileRequested(media, new_source_path)
|
||||
DeleteMediaRequested(media)
|
||||
UpsertMediaTranslationRequested(media, language, title, alt, caption)
|
||||
RebuildMediaFromFilesRequested(project)
|
||||
@@ -158,6 +159,28 @@ rule UpdateMedia {
|
||||
ensures: SearchIndexUpdated(media)
|
||||
}
|
||||
|
||||
rule ReplaceMediaFile {
|
||||
when: ReplaceMediaFileRequested(media, new_source_path)
|
||||
-- Replaces the binary at media.file_path with the new source file,
|
||||
-- keeping the same path/id. Sidecar metadata (title/alt/etc.) is preserved.
|
||||
let checksum = md5(read(new_source_path))
|
||||
-- Identical content (checksum = media.checksum): no-op, nothing rewritten.
|
||||
-- Otherwise the old file is backed up to {path}.bak (restored on failure,
|
||||
-- removed on success) and the row is updated from the new file:
|
||||
if checksum != media.checksum:
|
||||
ensures: media.checksum = checksum
|
||||
ensures: media.size = file_size(new_source_path)
|
||||
ensures: media.updated_at = now
|
||||
ensures: MediaDimensionsUpdated(media)
|
||||
-- width/height re-read from the new image
|
||||
ensures: SidecarWritten(media)
|
||||
if media.is_image:
|
||||
ensures: ThumbnailsRegenerated(media)
|
||||
-- Synchronous (awaited), not fire-and-forget
|
||||
ensures: SearchIndexUpdated(media)
|
||||
-- See engine_side_effects.allium ReplaceMediaFileSideEffects
|
||||
}
|
||||
|
||||
rule DeleteMedia {
|
||||
when: DeleteMediaRequested(media)
|
||||
ensures: not exists media
|
||||
|
||||
@@ -14,6 +14,7 @@ surface MetadataMaintenanceSurface {
|
||||
provides:
|
||||
MetadataDiffRequested(project)
|
||||
RebuildFromFilesystemRequested(project, entity_type)
|
||||
RepairMetadataDiffItemRequested(project, direction, item)
|
||||
}
|
||||
|
||||
value DiffField {
|
||||
@@ -23,7 +24,8 @@ value DiffField {
|
||||
}
|
||||
|
||||
value DiffReport {
|
||||
entity_type: String -- post, media, script, template
|
||||
entity_type: String -- post, post_translation, media,
|
||||
-- media_translation, script, template, embedding
|
||||
entity_id: String
|
||||
differences: List<DiffField>
|
||||
}
|
||||
@@ -66,7 +68,26 @@ rule RunMetadataDiff {
|
||||
if matching.count = 0:
|
||||
ensures: OrphanReport.created(file_path: file)
|
||||
|
||||
-- Same pattern for media sidecar files, scripts, templates
|
||||
-- Same pattern for media sidecars (media), media translation sidecars
|
||||
-- (media_translation), scripts, templates, and embeddings.
|
||||
-- Embedding diffs compare the stored content_hash against the live post
|
||||
-- content to detect vectors that need recomputation.
|
||||
}
|
||||
|
||||
rule RepairMetadataDiffItem {
|
||||
when: RepairMetadataDiffItemRequested(project, direction, item)
|
||||
-- Resolves a single diff in one direction.
|
||||
-- direction = file_to_db (filesystem wins) | db_to_file (database wins)
|
||||
-- Dispatched per item.entity_type:
|
||||
-- project | categories | category_meta | publishing -> project metadata sync/flush
|
||||
-- post -> sync_post_from_file / rewrite_published_post
|
||||
-- post_translation -> sync_post_translation_from_file / rewrite_published_post_translation
|
||||
-- media -> sync_media_from_sidecar / sync_media_sidecar
|
||||
-- media_translation -> sync_media_translation_from_sidecar / sync_media_translation_sidecar
|
||||
-- script -> sync_script_from_file / sync_published_script_file
|
||||
-- template -> sync_template_from_file / sync_published_template_file
|
||||
-- embedding -> sync_post (file_to_db) / refresh_snapshot (db_to_file)
|
||||
-- Unknown entity_type or direction -> unsupported error.
|
||||
}
|
||||
|
||||
rule RebuildFromFilesystem {
|
||||
|
||||
@@ -55,6 +55,7 @@ surface PostControlSurface {
|
||||
PublishPostRequested(post)
|
||||
DeletePostRequested(post)
|
||||
ArchivePostRequested(post)
|
||||
DiscardPostChangesRequested(post)
|
||||
}
|
||||
|
||||
surface PostFilePathSurface {
|
||||
@@ -100,6 +101,15 @@ entity Post {
|
||||
updated_at: Timestamp
|
||||
published_at: Timestamp?
|
||||
|
||||
-- Published snapshot: copy of title/content/tags/categories/excerpt as of
|
||||
-- the last publish. Used by changes_affect_published_content to decide when
|
||||
-- an edit reopens a published post to draft (see ReopenPublishedPost).
|
||||
published_title: String?
|
||||
published_content: String?
|
||||
published_tags: String?
|
||||
published_categories: String?
|
||||
published_excerpt: String?
|
||||
|
||||
-- Relationships
|
||||
translations: PostTranslation with canonical_post = this
|
||||
linked_media: PostMediaLink with post = this
|
||||
@@ -218,6 +228,20 @@ rule ArchivePost {
|
||||
ensures: post.status = archived
|
||||
}
|
||||
|
||||
rule DiscardPostChanges {
|
||||
when: DiscardPostChangesRequested(post)
|
||||
requires: post.file_path != ""
|
||||
-- Only posts with a published file on disk can be discarded;
|
||||
-- a never-published draft has no file version to restore.
|
||||
-- Re-reads the published .md file and upserts the DB record from it,
|
||||
-- discarding unsaved draft edits.
|
||||
ensures: post.content = null
|
||||
ensures: post.status = published
|
||||
ensures: PostLinksUpdated(post)
|
||||
ensures: SearchIndexUpdated(post)
|
||||
-- See engine_side_effects.allium DiscardPostChangesSideEffects
|
||||
}
|
||||
|
||||
-- File format axioms
|
||||
|
||||
invariant FrontmatterRoundtrip {
|
||||
|
||||
207
specs/rendering.allium
Normal file
207
specs/rendering.allium
Normal file
@@ -0,0 +1,207 @@
|
||||
-- allium: 1
|
||||
-- bDS Rendering Subsystem
|
||||
-- Scope: core — template rendering shared by preview and generation
|
||||
-- Distilled from: lib/bds/rendering/{filters,labels,links_and_languages,
|
||||
-- metadata,post_rendering,list_archive}.ex
|
||||
|
||||
-- The rendering subsystem turns a post/list/not-found record plus project
|
||||
-- metadata into the assigns consumed by a Liquid template. It is shared by the
|
||||
-- preview server (on-demand) and static site generation (published .md files).
|
||||
-- Rendering language is the CONTENT language (post/project), never the UI locale.
|
||||
|
||||
use "./template.allium" as template
|
||||
use "./template_context.allium" as template_context
|
||||
use "./i18n.allium" as i18n
|
||||
use "./post.allium" as post
|
||||
|
||||
-- ─── Custom Liquid filters ───────────────────────────────────
|
||||
|
||||
-- The three custom filters available in the Liquid subset (see
|
||||
-- template.allium LiquidFilterSubset). Applied during template rendering.
|
||||
|
||||
surface RenderFilterSurface {
|
||||
facing _: TemplateRenderer
|
||||
|
||||
provides:
|
||||
I18nFilterApplied(value, language)
|
||||
MarkdownFilterApplied(value, post_id, language)
|
||||
SlugifyFilterApplied(value)
|
||||
}
|
||||
|
||||
rule I18nFilter {
|
||||
when: I18nFilterApplied(value, language)
|
||||
-- {{ "Key" | i18n }} -> localized "render" domain string for `language`.
|
||||
-- Empty/blank input returns empty string.
|
||||
ensures: result = lgettext(language, "render", trim(value))
|
||||
}
|
||||
|
||||
rule SlugifyFilter {
|
||||
when: SlugifyFilterApplied(value)
|
||||
-- {{ title | slugify }} -> URL-friendly slug (same Slug.slugify as posts).
|
||||
ensures: result = slugify(value)
|
||||
}
|
||||
|
||||
rule MarkdownFilter {
|
||||
when: MarkdownFilterApplied(value, post_id, language)
|
||||
-- {{ body | markdown }} pipeline, in order:
|
||||
-- 1. Expand built-in [[...]] macros (see ExpandBuiltinMacros)
|
||||
-- 2. Convert markdown to HTML (Earmark; errors degrade to partial HTML)
|
||||
-- 3. Rewrite href/src URLs to canonical post/media paths (see RewriteUrls)
|
||||
ensures: result = rewritten_html
|
||||
}
|
||||
|
||||
-- ─── Built-in macros ─────────────────────────────────────────
|
||||
|
||||
-- Macros use double-bracket syntax [[name param="value" ...]], expanded in
|
||||
-- markdown before HTML conversion. NOT Liquid tags. Each renders a bundled
|
||||
-- macro template (macros/{name}) in an isolated Liquid subscope.
|
||||
|
||||
value BuiltinMacro {
|
||||
name: String -- youtube | vimeo | gallery | photo_archive | tag_cloud
|
||||
params: Map<String, String>
|
||||
}
|
||||
|
||||
surface BuiltinMacroSurface {
|
||||
context macro: BuiltinMacro
|
||||
|
||||
exposes:
|
||||
macro.name
|
||||
macro.params
|
||||
}
|
||||
|
||||
rule ExpandBuiltinMacros {
|
||||
when: MacroEncountered(macro, language, post_id)
|
||||
-- Unknown macro names are left verbatim in the source.
|
||||
if macro.name = "youtube" or macro.name = "vimeo":
|
||||
-- Embeds video by `id`; localized default title when title param absent.
|
||||
ensures: MacroTemplateRendered(format("macros/{name}", name: macro.name))
|
||||
if macro.name = "gallery":
|
||||
-- Image gallery from the post's linked image media (ordered by sort).
|
||||
-- `columns` clamped to 1..6 (default 3). Empty when no post_id/images.
|
||||
ensures: MacroTemplateRendered("macros/gallery")
|
||||
if macro.name = "photo_archive":
|
||||
-- Month-grouped image archive for the project (optional year/month filter).
|
||||
ensures: MacroTemplateRendered("macros/photo-archive")
|
||||
if macro.name = "tag_cloud":
|
||||
-- Weighted tag cloud from post tag counts; per-tag colours from Tag rows.
|
||||
ensures: MacroTemplateRendered("macros/tag-cloud")
|
||||
}
|
||||
|
||||
invariant MacroIsolation {
|
||||
-- Macro templates render in an isolated Liquid subscope so macro-local
|
||||
-- assigns never leak into the surrounding template context.
|
||||
}
|
||||
|
||||
-- ─── URL rewriting ───────────────────────────────────────────
|
||||
|
||||
rule RewriteUrls {
|
||||
when: RenderedHtmlProduced(html, canonical_post_paths, canonical_media_paths)
|
||||
-- Rewrites href= and src= attribute values in rendered HTML.
|
||||
-- External/special URLs (scheme:, //, #) are left untouched.
|
||||
-- Internal post references (/post/{slug}, /posts/{slug}, dated paths) map to
|
||||
-- the post's canonical dated path; query/fragment suffixes are preserved.
|
||||
-- Internal /media/YYYY/MM/{file} references map to canonical media paths.
|
||||
ensures: AttributesRewritten(html)
|
||||
}
|
||||
|
||||
-- ─── Links and languages ─────────────────────────────────────
|
||||
|
||||
value LinkContext {
|
||||
href: String
|
||||
title: String
|
||||
display_slug: String
|
||||
language: String
|
||||
}
|
||||
|
||||
rule ResolveLanguagePrefix {
|
||||
when: LanguagePrefixRequested(language, main_language)
|
||||
-- "" for the main language (and nil/blank); "/{language}" otherwise.
|
||||
if language = main_language:
|
||||
ensures: prefix = ""
|
||||
else:
|
||||
ensures: prefix = format("/{lang}", lang: language)
|
||||
}
|
||||
|
||||
rule CollectLinkContexts {
|
||||
when: LinkContextsRequested(project, post_id, direction)
|
||||
-- direction = incoming (backlinks) | outgoing.
|
||||
-- One LinkContext per linked post that still exists; missing targets dropped.
|
||||
-- href = canonical post path, language normalized to main when unset.
|
||||
ensures: List<LinkContext>
|
||||
}
|
||||
|
||||
-- ─── Render labels ───────────────────────────────────────────
|
||||
|
||||
value RenderLabels {
|
||||
-- Localized strings for rendered/preview output, resolved in the "render"
|
||||
-- gettext domain for the CONTENT language (not the UI locale). Includes
|
||||
-- taxonomy, backlinks, archive, pagination, calendar, search, not-found,
|
||||
-- and macro fallback labels. Month names resolved 1..12 per language.
|
||||
}
|
||||
|
||||
invariant LabelsUseContentLanguage {
|
||||
-- RenderLabels and the i18n filter resolve against the content/render
|
||||
-- language, consistent with i18n.allium's split-localization rule.
|
||||
}
|
||||
|
||||
-- ─── Post render assigns ─────────────────────────────────────
|
||||
|
||||
-- The full assigns map for a single post template, assembled from the post
|
||||
-- record (Post or PostTranslation) and project metadata.
|
||||
|
||||
value PostRenderAssigns {
|
||||
language: String
|
||||
language_prefix: String
|
||||
page_title: String?
|
||||
pico_stylesheet_href: String
|
||||
blog_languages: List<i18n/RenderLanguage>
|
||||
alternate_links: List<AlternateLink> -- hreflang alternates for translations
|
||||
menu_items: List<template_context/MenuItem>
|
||||
post_categories: List<String>
|
||||
post_tags: List<String>
|
||||
tag_color_by_name: Map<String, String?>
|
||||
backlinks: List<LinkContext>
|
||||
canonical_post_path_by_slug: Map<String, String>
|
||||
canonical_media_path_by_source_path: Map<String, String>
|
||||
post_data_json_by_id: String -- PostData JSON for client widgets
|
||||
post: template_context/PostContext -- includes incoming/outgoing links
|
||||
labels: RenderLabels
|
||||
calendar_initial_year: Integer?
|
||||
calendar_initial_month: Integer?
|
||||
}
|
||||
|
||||
value AlternateLink {
|
||||
language: String
|
||||
href: String
|
||||
}
|
||||
|
||||
rule BuildPostAssigns {
|
||||
when: PostAssignsRequested(project, assigns)
|
||||
-- Loads the post/translation record, renders its markdown body (macros +
|
||||
-- HTML + URL rewrite), collects incoming/outgoing links, and resolves all
|
||||
-- metadata-derived assigns (menu, languages, alternates, tag colours,
|
||||
-- calendar bounds, labels) for the post's language.
|
||||
ensures: PostRenderAssigns
|
||||
}
|
||||
|
||||
rule BuildNotFoundAssigns {
|
||||
when: NotFoundAssignsRequested(project, assigns)
|
||||
-- Assigns for the 404 page: page_title defaults to "404", no alternates,
|
||||
-- but shares language/menu/blog_languages/stylesheet/labels with posts.
|
||||
-- Consumed by the not-found template (see generation.allium 404.html).
|
||||
ensures: NotFoundRenderAssigns
|
||||
}
|
||||
|
||||
rule BuildListAssigns {
|
||||
when: ListAssignsRequested(project, assigns)
|
||||
-- Assigns for list/archive pages (home, category, tag, date archives):
|
||||
-- paginated post list plus shared metadata-derived assigns.
|
||||
ensures: ListRenderAssigns
|
||||
}
|
||||
|
||||
invariant SharedRenderPathForPreviewAndGeneration {
|
||||
-- Preview and generation produce identical HTML for the same input because
|
||||
-- both build assigns through this subsystem and render via the same Liquid
|
||||
-- subset. They differ only in content SOURCE (see preview.allium
|
||||
-- PreviewDraftOverlay and generation.allium GenerationPublishedOnly).
|
||||
}
|
||||
@@ -36,6 +36,7 @@ surface TaskRuntimeSurface {
|
||||
TaskWorkCompleted(task)
|
||||
TaskWorkFailed(task, error_message)
|
||||
ProgressReported(task, value, message)
|
||||
FinishedTaskEvictionDue()
|
||||
}
|
||||
|
||||
surface TaskSurface {
|
||||
@@ -54,6 +55,8 @@ surface TaskSurface {
|
||||
config {
|
||||
max_concurrent: Integer = 3
|
||||
progress_throttle: Duration = 250.milliseconds
|
||||
finished_task_ttl: Duration = 1.hour
|
||||
recent_finished_limit: Integer = 10
|
||||
}
|
||||
|
||||
invariant MaxConcurrency {
|
||||
@@ -112,6 +115,23 @@ invariant ProgressThrottled {
|
||||
-- At most one progress event per 250ms per task
|
||||
}
|
||||
|
||||
invariant FinishedTaskRetention {
|
||||
-- The status snapshot surfaces only the most recent finished tasks:
|
||||
-- completed/failed/cancelled tasks beyond config.recent_finished_limit
|
||||
-- (newest first) are not shown.
|
||||
let finished = Tasks where status in {completed, failed, cancelled}
|
||||
-- At most config.recent_finished_limit finished tasks are reported.
|
||||
}
|
||||
|
||||
rule EvictFinishedTasks {
|
||||
when: FinishedTaskEvictionDue()
|
||||
-- Periodic sweep (every config.finished_task_ttl). A finished task whose
|
||||
-- finished_at is older than config.finished_task_ttl is dropped from state.
|
||||
for task in Tasks where status in {completed, failed, cancelled}:
|
||||
if now - task.finished_at >= config.finished_task_ttl:
|
||||
ensures: not exists task
|
||||
}
|
||||
|
||||
-- External tasks: lifecycle controlled by caller (e.g., renderer-side scripts)
|
||||
rule RegisterExternalTask {
|
||||
when: RegisterExternalTaskRequested(name)
|
||||
|
||||
Reference in New Issue
Block a user