-- 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 -- Always written, even if empty categories: List -- 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 -- Always written, even if empty linkedPostIds: List -- 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 } 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 { 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) } -- ============================================================================ -- 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 dateCreated: Timestamp dateModified: 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 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 } }