Files
RuDS/specs/script.allium

382 lines
16 KiB
Plaintext

-- allium: 1
-- bDS Scripting System
-- Scope: core (Wave 6 — scripting behaviour and file contracts)
-- Distilled from: src/main/engine/ScriptEngine.ts, schema.ts
-- Lua is the normative scripting language for user-authored scripts in the
-- rewrite. The concrete embedding strategy remains an implementation choice;
-- only the behavioural contract is normative here.
config {
macro_timeout: Duration = 10.seconds
transform_max_toasts_per_script: Integer = 5
transform_max_toasts_total: Integer = 20
transform_max_toast_length: Integer = 300
blogmark_max_title_length: Integer = 200
blogmark_max_url_length: Integer = 2048
}
enum ScriptStatus {
draft
published
}
entity Script {
project_id: String
slug: String
title: String
kind: macro | utility | transform
entrypoint: String -- named Lua function used as the script entrypoint
enabled: Boolean
status: ScriptStatus
content: String?
version: Integer
file_path: String
created_at: Timestamp
updated_at: Timestamp
-- Derived
content_location: if status = published: file_path else: content
transitions status {
draft -> published
published -> draft
}
}
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
provides:
CreateScriptRequested(project, title, kind, content, entrypoint)
CreateAndPublishScriptRequested(project, title, kind, content, entrypoint)
UpdateScriptRequested(script, changes)
PublishScriptRequested(script)
DeleteScriptRequested(script)
RunUtilityRequested(script)
MacroExpansionRequested(script, template_context)
BlogmarkReceived(data)
RebuildScriptsFromFilesRequested(project)
}
surface ScriptRuntimeSurface {
facing _: ScriptRuntime
provides:
ValidateScript(source)
ExecuteScriptRequested(script, entrypoint, args, progress_sink)
@guarantee SandboxedExecution
-- User-authored Lua executes from a sandboxed runtime state.
-- Filesystem mutation, process control, package loading, and other
-- unrestricted host capabilities are unavailable unless explicitly
-- re-exposed by the host application.
@guarantee ExplicitHostCapabilities
-- Host-provided functions are exposed only through an explicit bds.*
-- capability table, never through ambient global access.
@guarantee CoreHostApiCompatibility
-- The core API matches the bDS2 capability names below. Methods are
-- bound to the active project unless an explicit project id is part of
-- the method. Arguments and return tables use the canonical types in
-- the generated scripting reference.
--
-- bds.app: copy_to_clipboard, get_data_paths,
-- get_blogmark_bookmarklet, get_system_language,
-- get_default_project_path, get_title_bar_metrics, log,
-- notify_renderer_ready, open_folder, read_project_metadata,
-- select_folder, set_preview_post_target, show_item_in_folder,
-- trigger_menu_action.
-- bds.projects: create, delete, delete_with_data, get, get_all,
-- get_active, set_active, update.
-- bds.meta: get_project_metadata, update_project_metadata,
-- set_project_metadata, add_category, remove_category, add_tag,
-- remove_tag, get_categories, get_tags,
-- get_publishing_preferences, set_publishing_preferences,
-- clear_publishing_preferences, sync_on_startup.
-- bds.posts: create, discard, filter, generate_unique_slug,
-- get_by_status, get_by_year_month, get_dashboard_stats,
-- get_linked_by, get_links_to, get_preview_url, update, delete, get,
-- get_all, get_by_slug, get_categories, get_categories_with_counts,
-- get_tags, get_tags_with_counts, get_translation, get_translations,
-- has_published_version, is_slug_available, publish,
-- publish_translation, rebuild_from_files, rebuild_links,
-- reindex_text, search.
-- bds.media: delete_translation, filter, import, get_by_year_month,
-- get_file_path, update, delete, get, get_all, get_tags,
-- get_tags_with_counts, get_thumbnail, get_translation,
-- get_translations, get_url, rebuild_from_files,
-- regenerate_missing_thumbnails, regenerate_thumbnails, reindex_text,
-- replace_file, search, upsert_translation.
-- bds.scripts: create, update, delete, get, get_all, publish,
-- rebuild_from_files.
-- bds.templates: create, update, delete, get, get_all, publish,
-- get_enabled_by_kind, rebuild_from_files, validate.
-- bds.tags: create, update, delete, get, get_all, get_by_name,
-- get_posts_with_tag, get_with_counts, merge, rename,
-- sync_from_posts.
-- bds.tasks: get, status_snapshot, cancel, get_all, get_running,
-- clear_completed.
-- bds.publish: upload_site.
-- bds.chat: detect_post_language, analyze_post, translate_post,
-- analyze_media_image, detect_media_language,
-- translate_media_metadata. Despite its historical namespace, this is
-- the core one-shot AI bridge; conversational chat remains extension.
-- bds.report_progress(payload) reports managed-job progress. The Rust
-- host may retain bds.app.progress and bds.app.toast as additive APIs.
-- bds.sync and bds.embeddings belong to the extension contract.
@guarantee HostApiFailureValues
-- Host failures do not expose implementation exceptions across the Lua
-- boundary. Lookup/create/update methods return nil on failure and
-- boolean command methods return false, as declared by the generated
-- method signature. Returned records contain only serializable public
-- fields; timestamps are ISO-8601 strings and enum atoms are strings.
@guarantee GeneratedApiReferenceStaysInSync
-- One machine-readable method/type manifest generates the bundled Lua
-- API reference and editor completion data. A verification test fails
-- when the exposed capability table, manifest, or generated reference
-- diverge. Canonical macro, transform, and utility examples are bundled
-- under docs/scripting/ and execute against the same host API.
@guarantee MacroTimeout
-- Macro execution has a short timeout budget of config.macro_timeout.
@guarantee ManagedBatchExecution
-- Utility and transform scripts execute as managed jobs.
-- The contract does not define a fixed wall-clock limit for those
-- jobs because batch work can legitimately scale with project size.
-- Progress reporting, operator cancellation, and host orchestration
-- govern their lifecycle instead of a fixed timeout.
@guarantee ProgressFeedback
-- Long-running utility and transform scripts may emit progress updates
-- through explicit host APIs during execution.
-- Progress reporting is cooperative and flows through the supplied
-- progress sink rather than ambient global side effects.
@guarantee ScriptOutputRouting
-- print(…) calls and the explicit bds.app.log(…) API append lines to a
-- host-routed script output stream: the desktop app's Output panel,
-- stdout in the CLI, and the host logger when no sink is attached —
-- never the ambient console.
@guarantee BatchCancellation
-- Managed utility and transform jobs can be cancelled by the host
-- operator boundary.
}
invariant UniqueScriptSlug {
-- Slug uniqueness is scoped per project, not globally.
for a in Scripts:
for b in Scripts:
a != b and a.project_id = b.project_id implies a.slug != b.slug
}
invariant ScriptFileLayout {
for s in Scripts where file_path != "":
s.file_path = format("scripts/{slug}.lua", slug: s.slug)
}
-- Script files use standard --- YAML frontmatter
rule CreateScript {
when: CreateScriptRequested(project, title, kind, content, entrypoint)
let slug = slugify(title)
-- Creates a draft script: content stored in DB, no file written yet
ensures:
let new_script = Script.created(
project_id: project.id,
slug: slug,
title: title,
kind: kind,
content: content,
entrypoint: entrypoint ?? if kind = macro: "render" else: "main",
status: draft,
enabled: true,
version: 1,
file_path: ""
)
new_script.status = draft
}
rule UpdateScript {
when: UpdateScriptRequested(script, changes)
ensures: ScriptFieldsUpdated(script, changes)
ensures: script.updated_at = now
ensures: script.version = script.version + 1
}
rule ReopenPublishedScript {
when: UpdateScriptRequested(script, changes)
requires: script.status = published
requires: script_changes_affect_published_output(changes)
ensures: script.status = draft
}
rule CreateAndPublishScript {
-- Alternative creation path: create + immediately publish (file written)
-- Some implementations may expose this as a single user action
when: CreateAndPublishScriptRequested(project, title, kind, content, entrypoint)
let slug = slugify(title)
requires: ValidateScript(content) = valid
ensures:
let new_script = Script.created(
project_id: project.id,
slug: slug,
title: title,
kind: kind,
content: null,
entrypoint: entrypoint ?? if kind = macro: "render" else: "main",
status: published,
enabled: true,
version: 1,
file_path: format("scripts/{slug}.lua", slug: slug)
)
ScriptFileWritten(new_script)
}
rule PublishScript {
when: PublishScriptRequested(script)
requires: script.status = draft
requires: ValidateScript(script.content) = valid
-- Lua parsing must succeed before a script can be published
ensures: script.status = published
ensures: ScriptFileWritten(script)
ensures: script.content = null
}
rule DeleteScript {
when: DeleteScriptRequested(script)
ensures: not exists script
ensures: ScriptFileDeleted(script)
}
-- Script execution contracts by kind
rule ExecuteMacro {
when: MacroExpansionRequested(script, template_context)
requires: script.kind = macro
requires: script.enabled = true
requires: script.entrypoint != ""
-- Macro scripts are invoked during template rendering
-- via [[slug param1=value1 param2=value2]] syntax in post content
-- Unknown macro names are resolved against enabled macro scripts by slug.
-- They receive named parameters plus template_context.env fields that
-- include isPreview, mainLanguage, languagePrefix, hook, source.kind,
-- and translations.
-- They return HTML and run sequentially with config.macro_timeout per
-- invocation.
-- Macro failures degrade to empty output for that invocation and do not
-- abort rendering of the surrounding page.
ensures: MacroOutputProduced(script, html_output)
}
rule ExecuteUtility {
when: RunUtilityRequested(script)
requires: script.kind = utility
requires: script.enabled = true
requires: script.entrypoint != ""
-- Utility scripts commonly perform long-running data manipulation work.
-- They are manually started by an operator action, run as managed jobs,
-- may issue host-backed API calls, may emit progress during execution,
-- and may be cancelled by the operator.
ensures: UtilityOutputProduced(script, stdout)
}
rule ExecuteTransform {
when: BlogmarkReceived(data)
-- Transform scripts run sequentially on blogmark deep link data
-- Input: title, content, tags, categories, source url
-- Each transform can modify the data before post creation.
-- Execution uses the same managed job host API contract as other batch
-- scripts and may report progress while mass-processing remote or local
-- content.
let transforms = Scripts where project_id = data.project_id and kind = transform and enabled = true
for t in ordered_by(transforms, s => s.updated_at, s => s.slug, s => s.id):
requires: t.entrypoint != ""
ensures: TransformApplied(t, data)
@guarantee BlogmarkInputHardening
-- The deep link is sanitized before transforms run. Title and url have
-- control characters stripped and are trimmed. The url is kept only
-- when it is http(s) with a host, after removing any credentials and
-- fragment, and at most config.blogmark_max_url_length characters;
-- otherwise it is dropped so it can never reach the post body. The
-- title is capped at config.blogmark_max_title_length and falls back to
-- the url host when blank.
@guarantee BlogmarkDefaultBody
-- When the deep link supplies no content, the post body defaults to a
-- markdown link [title](url) to the bookmarked page, with the title
-- escaped, so transforms operate on the same candidate whether or not
-- an explicit body was provided. Explicit content always wins.
@guarantee BlogmarkProjectTargeting
-- A link's own project_id determines the target project. When it names a
-- project that exists and differs from the active one, the shell
-- switches to it before importing so the new post is shown in context.
-- When it names a project that does not exist in this install (e.g. a
-- bookmarklet copied from another instance) the import is refused with
-- an error — the post is never redirected into a different project.
-- With no link project_id, the active project is used; with no link
-- project_id and no active project the import is refused with a warning.
@guarantee BlogmarkColdStartDelivery
-- A deep link that arrives before the shell is ready (e.g. the
-- bookmarklet launching the app) is queued and replayed only after the
-- client has finished initialising and rehydrating its stored
-- workbench session, so the post is created and the editor it opens is
-- not overwritten by the session restore. A session restore arriving
-- after a deep link (e.g. when the link switched projects) preserves
-- the newly opened editor tab as the active tab.
@guarantee TransformTrigger
-- Transform scripts are triggered automatically by blogmark import.
-- Each script receives the current post candidate plus a context with
-- source='blogmark' and the originating URL.
@guarantee TransformPipelineContinuation
-- Transform errors are captured per script and do not roll back the
-- last valid post state produced by earlier transforms.
-- The pipeline continues with subsequent enabled transforms.
@guarantee TransformToastBudget
-- Transform scripts may emit toast feedback.
-- At most config.transform_max_toasts_per_script toasts are accepted
-- from any one transform, with a total budget of
-- config.transform_max_toasts_total across the pipeline.
-- Individual toast messages are truncated to
-- config.transform_max_toast_length characters.
@guidance
-- RuDS accepts only ruds://new-post deep links from browser bookmarks.
-- bds2:// remains assigned to bDS2 so both applications can be installed.
-- Ordering is deterministic: updated_at, then slug, then id
}
rule RebuildScriptsFromFiles {
when: RebuildScriptsFromFilesRequested(project)
for file in scan_directory(project.public_dir + "/scripts", "*.lua"):
let parsed = parse_script_file(file)
ensures: Script.created(parsed)
}