-- 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)
}
