Files
bDS2/specs/frontmatter.allium

443 lines
14 KiB
Plaintext

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