-- 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.sync: check_availability, get_repo_state, get_status, -- get_history, get_remote_state, fetch, pull, push, commit_all. -- 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.embeddings: compute_similarities, dismiss_pair, find_duplicates, -- find_similar, get_progress, index_unindexed_posts, suggest_tags. @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) if changes.slug != null: ensures: script.slug = unique_slug(slugify(changes.slug), excluding: script) if script.file_path != "": ensures: script.file_path = format("scripts/{slug}.lua", slug: script.slug) ensures: ScriptFileWritten(script) ensures: PreviousScriptFileDeleted(script) 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) }