-- allium: 1
-- bDS Frontmatter Specifications
-- Scope: core (Wave 1 — exact file format compatibility)
-- Distilled from: ../bDS/src/main/engine/postFileUtils.ts,
--   TemplateEngine.ts, ScriptEngine.ts, MediaEngine.ts
--
-- This document specifies the exact YAML frontmatter format for all
-- file types. The rewrite must read and write these formats compatibly
-- with existing bDS content.

surface FrontmatterPersistenceSurface {
    facing _: ContentPersistenceRuntime

    provides:
        PublishPostRequested(post)
        PublishTemplateRequested(template)
        PublishScriptRequested(script)
}

surface PostFrontmatterSurface {
    context frontmatter: PostFrontmatter

    exposes:
        frontmatter.id
        frontmatter.title
        frontmatter.slug
        frontmatter.status
        frontmatter.publishedAt
        frontmatter.tags
        frontmatter.categories
}

surface MediaSidecarSurface {
    context sidecar: MediaSidecar

    exposes:
        sidecar.id
        sidecar.originalName
        sidecar.mimeType
        sidecar.width
        sidecar.height
        sidecar.updatedAt
}

surface TemplateFrontmatterSurface {
    context frontmatter: TemplateFrontmatter

    exposes:
        frontmatter.id
        frontmatter.slug
        frontmatter.kind
        frontmatter.enabled
        frontmatter.version
}

surface ScriptFrontmatterSurface {
    context frontmatter: ScriptFrontmatter

    exposes:
        frontmatter.id
        frontmatter.slug
        frontmatter.kind
        frontmatter.entrypoint
        frontmatter.enabled
        frontmatter.version
}

surface MenuOpmlSurface {
    context document: MenuOpml

    exposes:
        document.header.title
        document.header.dateCreated
        document.header.dateModified
        for item in document.body:
            item.kind
            item.label
            item.slug
}

config {
    script_extension: String = "lua"
}

-- ============================================================================
-- POST FILE FORMAT
-- ============================================================================

value PostFrontmatter {
    -- File path: posts/{YYYY}/{MM}/{slug}.md
    -- All keys serialized as camelCase in YAML frontmatter
    id: String              -- UUID v4
    title: String
    slug: String
    excerpt: String?        -- Optional, only written if present
    status: draft | published | archived
    author: String?         -- Only written if present
    language: String?       -- Only written if present (ISO 639-1)
    doNotTranslate: Boolean -- Only written when true
    templateSlug: String?   -- Only written if present
    createdAt: Timestamp    -- Unix timestamp in milliseconds
    updatedAt: Timestamp    -- Unix timestamp in milliseconds
    publishedAt: Timestamp? -- Only written if published
    tags: List<String>      -- Always written, even if empty
    categories: List<String> -- Always written, even if empty
}

value TranslationFrontmatter {
    -- File path: posts/{YYYY}/{MM}/{slug}.{language}.md
    -- Translation files carry their own publication state and timestamps
    -- so that each translation can be rebuilt independently.
    -- All keys serialized as camelCase in YAML frontmatter
    id: String              -- UUID v4
    translationFor: String  -- Canonical post UUID
    language: String        -- ISO 639-1 language code
    title: String           -- Translated title
    excerpt: String?        -- Only written when the translated excerpt differs
    status: draft | published
    createdAt: Timestamp    -- Unix timestamp in milliseconds
    updatedAt: Timestamp    -- Unix timestamp in milliseconds
    publishedAt: Timestamp  -- Canonical post's publishedAt at time of publish
}

surface TranslationFrontmatterSurface {
    context frontmatter: TranslationFrontmatter

    exposes:
        frontmatter.id
        frontmatter.translationFor
        frontmatter.language
        frontmatter.title
        frontmatter.excerpt when frontmatter.excerpt != null
        frontmatter.status
        frontmatter.createdAt
        frontmatter.updatedAt
        frontmatter.publishedAt
}

invariant PostFileLayout {
    -- Posts are stored in date-based directory structure
    -- YYYY and MM derived from created_at (zero-padded)
    for p in Posts where file_path != "":
        p.file_path = format("posts/{yyyy}/{mm}/{slug}.md",
            yyyy: p.created_at.year,
            mm: p.created_at.month_padded,
            slug: p.slug)
}

invariant PostTranslationFileLayout {
    -- Translations use the same directory structure with language suffix
    for t in PostTranslations where file_path != "":
        t.file_path = format("posts/{yyyy}/{mm}/{slug}.{lang}.md",
            yyyy: t.canonical_post.created_at.year,
            mm: t.canonical_post.created_at.month_padded,
            slug: t.canonical_post.slug,
            lang: t.language)
}

invariant TranslationFrontmatterRoundtrip {
    -- Translation files carry status and timestamps explicitly.
    -- On rebuild, these fields are read back directly; fallback to canonical
    -- post values applies only when fields are absent (legacy files).
    for t in PostTranslations where file_path != "":
        parse_frontmatter(read_file(t.file_path)) = translation_frontmatter_fields(t)
}

rule WritePostFile {
    when: PublishPostRequested(post)
    ensures: FileWritten(
        path: post.file_path,
        content: format_post_file(post)
    )
    ensures: post.content = null
        -- Content moved from DB to filesystem
}

-- ============================================================================
-- MEDIA SIDECAR FORMAT
-- ============================================================================

value MediaSidecar {
    -- File path: {binary_path}.meta (e.g., media/2024/03/a1b2c3d4.jpg.meta)
    -- Binary file at: media/{YYYY}/{MM}/{uuid}.{ext}
    -- Format: YAML-like key-value wrapped in --- delimiters (gray-matter style, hand-built serializer)
    -- Note: 'filename' is NOT written to sidecar — it is implicit from the binary path
    -- All keys serialized as camelCase
    id: String              -- UUID v4
    originalName: String    -- Original uploaded filename
    mimeType: String
    size: Integer           -- Bytes
    width: Integer?
    height: Integer?
    title: String?          -- Only written if present
    alt: String?            -- Only written if present
    caption: String?        -- Only written if present
    author: String?         -- Only written if present
    language: String?       -- Only written if present
    tags: List<String>      -- Always written, even if empty
    linkedPostIds: List<String> -- UUIDs of posts that reference this media
    createdAt: Timestamp
    updatedAt: Timestamp
}

invariant MediaSidecarLayout {
    for m in Media:
        m.sidecar_path = format("{binary_path}.meta", binary_path: m.file_path)
}

-- ============================================================================
-- TEMPLATE FILE FORMAT
-- ============================================================================

value TemplateFrontmatter {
    -- File path: templates/{slug}.liquid
    -- All keys serialized as camelCase in YAML frontmatter
    id: String              -- UUID v4
    projectId: String       -- Scoped to project
    slug: String
    title: String
    kind: post | list | not_found | partial
    enabled: Boolean
    version: Integer
    createdAt: Timestamp
    updatedAt: Timestamp
}

rule WriteTemplateFile {
    when: PublishTemplateRequested(template)
    requires: ValidateLiquid(template.content) = valid
    ensures: FileWritten(
        path: format("templates/{slug}.liquid", slug: template.slug),
        content: format_template_file(template)
    )
    ensures: template.content = null
}

-- ============================================================================
-- SCRIPT FILE FORMAT
-- ============================================================================

value ScriptFrontmatter {
    -- File path: scripts/{slug}.{extension}
    -- YAML frontmatter delimited by --- markers
    -- All keys serialized as camelCase in YAML frontmatter
    id: String              -- UUID v4
    projectId: String       -- Scoped to project
    slug: String
    title: String
    kind: macro | utility | transform
    entrypoint: String      -- Default: "render" for macros, "main" otherwise
    enabled: Boolean
    version: Integer
    createdAt: Timestamp
    updatedAt: Timestamp
}

rule WriteScriptFile {
    when: PublishScriptRequested(script)
    requires: ValidateScript(script.content) = valid
    ensures: FileWritten(
        path: format("scripts/{slug}.{extension}", slug: script.slug, extension: config.script_extension),
        content: format_script_file(script)
    )
    ensures: script.content = null
}

-- ============================================================================
-- TAGS FILE FORMAT
-- ============================================================================

value TagEntry {
    -- File path: meta/tags.json
    -- Stored as a bare JSON array (no wrapper object)
    -- Portable JSON format (no internal IDs), camelCase keys
    name: String
    color: String?
    postTemplateSlug: String?
}

invariant TagsFileFormat {
    -- Tags are stored as a bare sorted JSON array
    -- Sorted alphabetically by name (case-insensitive)
    parse_json(read_file("meta/tags.json")) =
        sort_by(tags, t => lowercase(t.name))
}

-- ============================================================================
-- PROJECT METADATA FILES
-- ============================================================================

value ProjectJson {
    -- File path: meta/project.json
    -- All keys serialized as camelCase
    name: String
    description: String?
    publicUrl: String?
    mainLanguage: String?
    defaultAuthor: String?
    maxPostsPerPage: Integer
    blogmarkCategory: String?
    picoTheme: String?
    semanticSimilarityEnabled: Boolean
    blogLanguages: List<String>
}

value CategoriesJson {
    -- File path: meta/categories.json
    -- Sorted list of category names
    categories: List<String>
}

value CategoryMetaJson {
    -- File path: meta/category-meta.json
    -- Per-category render settings
    categories: Map<String, CategorySettings>
}

value CategorySettings {
    renderInLists: Boolean
    showTitle: Boolean
    postTemplateSlug: String?
    listTemplateSlug: String?
}

value PublishingJson {
    -- File path: meta/publishing.json
    -- All keys serialized as camelCase
    sshHost: String?
    sshUser: String?
    sshRemotePath: String?
    sshMode: scp | rsync
}

invariant MetadataFileLayout {
    -- All metadata files in meta/ directory
    -- Each file is written atomically (temp file + rename)
    meta/project.json = serialize(ProjectJson)
    meta/categories.json = serialize(CategoriesJson)
    meta/category-meta.json = serialize(CategoryMetaJson)
    meta/publishing.json = serialize(PublishingJson)
    meta/menu.opml = serialize(Menu)
    meta/tags.json = serialize(List<TagEntry>)
}

-- ============================================================================
-- MENU FILE FORMAT
-- ============================================================================

value MenuOpml {
    -- File path: meta/menu.opml
    -- OPML 2.0 format with outline elements
    header: OpmlHeader
    body: List<MenuItem>
}

value OpmlHeader {
    title: String
    dateCreated: Timestamp
    dateModified: Timestamp
}

value MenuItem {
    kind: page | submenu | category_archive | home
    label: String
    slug: String?
    children: List<MenuItem>?
}

invariant MenuOpmlFormat {
    -- Menu is stored as OPML with Home always first
    -- Note: List literal syntax not supported in Allium
    -- Actual structure: header + body with MenuItem elements
}

-- ============================================================================
-- FILE FORMAT CONVENTIONS
-- ============================================================================

invariant TimestampFormat {
    -- Database: Unix milliseconds stored as INTEGER columns
    -- YAML frontmatter: ISO 8601 strings (e.g. 2024-03-15T14:30:00.000Z)
    -- Conversion on read: parse ISO 8601 → Unix ms
    -- Conversion on write: Unix ms → ISO 8601
}

invariant YamlFormatting {
    -- YAML frontmatter uses 2-space indentation
    -- Arrays use YAML list syntax: - item1\n- item2
    -- Strings with special characters are quoted
    -- Boolean values are lowercase: true/false
}

invariant CamelCaseKeys {
    -- All serialized keys in YAML frontmatter and JSON metadata use camelCase.
    -- Entity/DB fields use snake_case internally; the mapping happens at serialization.
}

invariant AtomicWrites {
    -- All file writes are atomic
    -- Write to temp file first, then rename
    -- Prevents corruption from interrupted writes
}

-- ============================================================================
-- FRONTmatter FIELD RULES
-- ============================================================================

invariant RequiredPostFields {
    -- These fields are ALWAYS written for posts
    for p in Posts:
        required_fields(p) = {
            id, title, slug, status, createdAt, updatedAt,
            tags, categories
        }
}

invariant ConditionalPostFields {
    -- These fields are ONLY written if truthy
    for p in Posts:
        conditional_fields(p) = {
            excerpt, author, language, templateSlug, publishedAt
        }
    -- doNotTranslate is only written when true
}

invariant RequiredMediaFields {
    -- These fields are ALWAYS written for media sidecars
    -- Note: 'filename' is NOT a sidecar field — it is the binary path itself
    for m in Media:
        required_fields(m) = {
            id, originalName, mimeType, size,
            createdAt, updatedAt, tags
        }
}

invariant ConditionalMediaFields {
    -- These fields are ONLY written if truthy
    for m in Media:
        conditional_fields(m) = {
            title, alt, caption, author, language, width, height
        }
}
