-- 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 { script_extension: String = "lua" macro_timeout: Duration = 10.seconds transform_max_toasts_per_script: Integer = 5 transform_max_toasts_total: Integer = 20 transform_max_toast_length: Integer = 300 } enum ScriptStatus { draft published } entity Script { 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(title, kind, content, entrypoint) CreateAndPublishScriptRequested(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 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 BatchCancellation -- Managed utility and transform jobs can be cancelled by the host -- operator boundary. } invariant UniqueScriptSlug { for a in Scripts: for b in Scripts: a != b implies a.slug != b.slug } invariant ScriptFileLayout { for s in Scripts where file_path != "": s.file_path = format("scripts/{slug}.{extension}", slug: s.slug, extension: config.script_extension) } -- Script files use standard --- YAML frontmatter rule CreateScript { when: CreateScriptRequested(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( 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(title, kind, content, entrypoint) let slug = slugify(title) requires: ValidateScript(content) = valid ensures: let new_script = Script.created( 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}.{extension}", slug: slug, extension: config.script_extension) ) 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 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 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 -- bds://new-post deep links from browser bookmarks -- Ordering is deterministic: updated_at, then slug, then id } rule RebuildScriptsFromFiles { when: RebuildScriptsFromFilesRequested(project) for file in scan_directory(project.effective_data_dir + "/scripts", "*." + config.script_extension): let parsed = parse_script_file(file) ensures: Script.created(parsed) }