-- 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.published_at frontmatter.tags frontmatter.categories } surface MediaSidecarSurface { context sidecar: MediaSidecar exposes: sidecar.id sidecar.original_name sidecar.mime_type sidecar.width sidecar.height sidecar.updated_at } 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.date_created document.header.date_modified 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 -- For translations: posts/{YYYY}/{MM}/{slug}.{language}.md 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) do_not_translate: Boolean -- Only written when true template_slug: String? -- Only written if present created_at: Timestamp -- Unix timestamp in milliseconds updated_at: Timestamp -- Unix timestamp in milliseconds published_at: Timestamp? -- Only written if published tags: List -- Always written, even if empty categories: List -- Always written, even if empty } 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) } 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 (hand-built, not gray-matter frontmatter) -- Note: 'filename' is NOT written to sidecar — it is implicit from the binary path id: String -- UUID v4 original_name: String -- Original uploaded filename mime_type: 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 -- Always written, even if empty created_at: Timestamp updated_at: 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 id: String -- UUID v4 slug: String title: String kind: post | list | not_found | partial enabled: Boolean version: Integer created_at: Timestamp updated_at: 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 id: String -- UUID v4 slug: String title: String kind: macro | utility | transform entrypoint: String -- Named Lua function used when invoking the script enabled: Boolean version: Integer created_at: Timestamp updated_at: 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 TagsFile { -- File path: meta/tags.json -- Portable JSON format (no internal IDs) tags: List } value TagEntry { name: String color: String? post_template_slug: String? } invariant TagsFileFormat { -- Tags are stored as a sorted JSON array -- Sorted alphabetically by name (case-insensitive) parse_json(read_file("meta/tags.json")) = { tags: sort_by(Tags, t => lowercase(t.name)) } } -- ============================================================================ -- PROJECT METADATA FILES -- ============================================================================ value ProjectJson { -- File path: meta/project.json name: String description: String? public_url: String? main_language: String? default_author: String? max_posts_per_page: Integer blogmark_category: String? pico_theme: String? semantic_similarity_enabled: Boolean blog_languages: List } value CategoriesJson { -- File path: meta/categories.json -- Sorted list of category names categories: List } value CategoryMetaJson { -- File path: meta/category-meta.json -- Per-category render settings categories: Map } value CategorySettings { render_in_lists: Boolean show_title: Boolean post_template_slug: String? list_template_slug: String? } value PublishingJson { -- File path: meta/publishing.json ssh_host: String? ssh_user: String? ssh_remote_path: String? ssh_mode: 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(TagsFile) } -- ============================================================================ -- MENU FILE FORMAT -- ============================================================================ value MenuOpml { -- File path: meta/menu.opml -- OPML 2.0 format with outline elements header: OpmlHeader body: List } value OpmlHeader { title: String date_created: Timestamp date_modified: Timestamp } value MenuItem { kind: page | submenu | category_archive | home label: String slug: String? children: List? } 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 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, created_at, updated_at, tags, categories } } invariant ConditionalPostFields { -- These fields are ONLY written if truthy for p in Posts: conditional_fields(p) = { excerpt, author, language, template_slug, published_at } -- do_not_translate 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, original_name, mime_type, size, created_at, updated_at, tags } } invariant ConditionalMediaFields { -- These fields are ONLY written if truthy for m in Media: conditional_fields(m) = { title, alt, caption, author, language, width, height } }