feat: additional specs

This commit is contained in:
2026-04-03 14:38:41 +02:00
parent 1c3a088efb
commit 95c97bce33
6 changed files with 1603 additions and 0 deletions

318
specs/frontmatter.allium Normal file
View File

@@ -0,0 +1,318 @@
-- 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 Rust implementation must read and write these
-- formats byte-for-byte compatible with the TypeScript implementation.
-- ============================================================================
-- 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<String> -- Always written, even if empty
categories: List<String> -- 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: media/{id}.md
-- Binary file at: media/{filename}
id: String -- UUID v4
filename: String -- Generated filename (e.g., photo_1234567890.jpg)
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<String> -- Always written, even if empty
created_at: Timestamp
updated_at: Timestamp
}
invariant MediaSidecarLayout {
for m in Media:
m.sidecar_path = format("media/{id}.md", id: m.id)
}
-- ============================================================================
-- 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}.lua (Rust) / {slug}.py (TypeScript legacy)
-- Note: TypeScript uses Python with triple-quote docstring delimiters
-- Rust uses Lua with standard YAML frontmatter
id: String -- UUID v4
slug: String
title: String
kind: macro | utility | transform
entrypoint: String -- Default: "render"
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}.lua", slug: script.slug),
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<TagEntry>
}
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<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 {
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<MenuItem>
}
value OpmlHeader {
title: String
date_created: Timestamp
date_modified: 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 {
-- All timestamps are Unix milliseconds (number in JavaScript)
-- Stored as ISO 8601 strings in YAML frontmatter
-- Example: 2024-03-15T14:30:00.000Z
}
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
for m in Media:
required_fields(m) = {
id, filename, 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
}
}

View File

@@ -0,0 +1,294 @@
-- allium: 1
-- bDS Media Processing Specification
-- Scope: core (Wave 1 — media import and processing)
-- Distilled from: ../bDS/src/main/engine/MediaEngine.ts,
-- mediaProcessing.ts, thumbnail generation logic
--
-- This document specifies the exact media processing behavior:
-- thumbnail generation, format conversion, EXIF handling, and file organization.
use "./media.allium" as media
use "./search.allium" as search
-- ============================================================================
-- MEDIA FILE ORGANIZATION
-- ============================================================================
value MediaFileLayout {
-- Binary assets stored in: media/{filename}
-- Sidecar metadata in: media/{id}.md
-- Thumbnails in: thumbnails/{id}.webp
-- Thumbnail source filename: thumbnails/{id}_source.{ext}
binary_path: String -- media/{filename}
sidecar_path: String -- media/{id}.md
thumbnail_path: String -- thumbnails/{id}.webp
thumbnail_source_path: String -- thumbnails/{id}_source.{ext}
}
invariant MediaFileNaming {
-- Original filename is preserved in original_name field
-- Stored filename is generated: {timestamp}_{random}.{ext}
-- Example: 1710512345678_abc123.jpg
for m in Media:
m.filename = format("{timestamp}_{random}.{ext}",
timestamp: m.created_at.milliseconds,
random: generate_random_string(6),
ext: file_extension(m.original_name))
}
-- ============================================================================
-- THUMBNAIL GENERATION
-- ============================================================================
config {
-- Thumbnail configuration
thumbnail_width: Integer = 400
thumbnail_height: Integer = 300
thumbnail_fit: String = "cover" -- "cover", "contain", "fill"
thumbnail_quality: Integer = 80 -- WEBP quality 1-100
thumbnail_format: String = "webp" -- Output format
}
rule GenerateThumbnail {
when: MediaImported(media)
requires: is_image(media.mime_type)
-- Generate thumbnail from source image
ensures: ThumbnailGenerated(
source: media.file_path,
destination: media.thumbnail_path,
width: config.thumbnail_width,
height: config.thumbnail_height,
fit: config.thumbnail_fit,
quality: config.thumbnail_quality,
format: config.thumbnail_format
)
ensures: ThumbnailSourceSaved(
source: media.file_path,
destination: media.thumbnail_source_path
)
}
-- Thumbnail generation algorithm
value ThumbnailGeneration {
-- 1. Load source image with EXIF orientation
-- 2. Auto-rotate based on EXIF orientation tag
-- 3. Resize using "cover" fit (crop to fill target dimensions)
-- 4. Encode as WEBP with specified quality
-- 5. Write to thumbnails/{id}.webp
-- Preserve original for source thumbnail
-- This allows regenerating thumbnails at different sizes later
}
invariant ThumbnailExifHandling {
-- EXIF orientation must be respected during thumbnail generation
-- Photos taken with phone cameras often have incorrect orientation
-- without proper EXIF handling
}
-- ============================================================================
-- IMAGE PROCESSING RULES
-- ============================================================================
value ImageProcessing {
-- Supported input formats:
input_formats: Set<String> = {
"image/jpeg", "image/png", "image/gif",
"image/webp", "image/tiff", "image/bmp",
"image/heic", "image/heif"
}
-- Output formats:
output_formats: Set<String> = {
"image/webp", -- Primary output
"image/jpeg", -- Fallback for non-WEBP browsers (not used in Rust version)
"image/png" -- For images with transparency
}
-- Processing rules:
-- 1. All images are converted to WEBP for thumbnails
-- 2. Original format is preserved for full-size assets
-- 3. EXIF data is stripped from thumbnails (privacy)
-- 4. EXIF orientation is applied during processing
}
rule ProcessImageMetadata {
when: MediaImported(media)
-- Extract image metadata
ensures: media.width = extract_width(source_file)
ensures: media.height = extract_height(source_file)
ensures: media.mime_type = detect_mime_type(source_file)
ensures: media.size = file_size(source_file)
}
-- ============================================================================
-- MEDIA TRANSLATION FILES
-- ============================================================================
value MediaTranslationFile {
-- File path: media/{id}/{language}.md
-- Format: YAML frontmatter with localized metadata
translation_for: String -- Canonical media ID
language: String -- ISO 639-1 code
title: String?
alt: String?
caption: String?
created_at: Timestamp
updated_at: Timestamp
}
invariant MediaTranslationFileLayout {
for t in MediaTranslations:
-- Stored in subdirectory per media item
t.file_path = format("media/{id}/{lang}.md",
id: t.translation_for,
lang: t.language)
}
-- ============================================================================
-- MEDIA IMPORT RULES
-- ============================================================================
rule ImportMedia {
when: ImportMediaRequested(source_path, project)
-- 1. Validate file type (must be image)
-- 2. Generate unique filename
-- 3. Copy to media/{filename}
-- 4. Generate thumbnail
-- 5. Create sidecar file with metadata
-- 6. Index for search (FTS5)
-- 7. Generate embedding (if enabled)
ensures: media/Media.created(
filename: generate_filename(),
original_name: basename(source_path),
mime_type: detect_mime_type(source_path),
size: file_size(source_path),
width: extract_width(source_path),
height: extract_height(source_path),
file_path: format("media/{filename}", filename: generated_filename),
sidecar_path: format("media/{id}.md", id: media_id),
checksum: sha256(source_path)
)
ensures: ThumbnailGenerated(media_id)
ensures: SearchIndexUpdated(media_id)
}
-- ============================================================================
-- MEDIA TAGGING
-- ============================================================================
invariant MediaTagsFormat {
-- Media tags are stored as JSON array in sidecar file
-- Tags are optional and only written if present
-- Same format as post tags
}
rule TagMedia {
when: TagMediaRequested(media, tags)
ensures: media.tags = tags
ensures: SidecarFileUpdated(media)
ensures: SearchIndexUpdated(media)
}
-- ============================================================================
-- MEDIA DELETION
-- ============================================================================
rule DeleteMedia {
when: DeleteMediaRequested(media)
-- 1. Remove from database
-- 2. Delete binary file
-- 3. Delete sidecar file
-- 4. Delete thumbnail files
-- 5. Remove from search index
-- 6. Remove from all post links
ensures: FileDeleted(media.file_path)
ensures: FileDeleted(media.sidecar_path)
ensures: FileDeleted(media.thumbnail_path)
ensures: FileDeleted(media.thumbnail_source_path)
ensures: SearchIndexRemoved(media)
ensures:
for p in media.linked_posts:
p.linked_media = p.linked_media - {media}
}
-- ============================================================================
-- MEDIA VALIDATION
-- ============================================================================
rule ValidateMedia {
when: ValidateMediaRequested(project)
-- Check for:
-- 1. Missing binary files
-- 2. Missing sidecar files
-- 3. Missing thumbnails
-- 4. Corrupted image files
-- 5. Orphan media (not linked to any post)
for m in project.media:
if not file_exists(m.file_path):
ensures: ValidationIssueReported(m, "missing_binary")
if not file_exists(m.sidecar_path):
ensures: ValidationIssueReported(m, "missing_sidecar")
if not file_exists(m.thumbnail_path):
ensures: ValidationIssueReported(m, "missing_thumbnail")
if not is_valid_image(m.file_path):
ensures: ValidationIssueReported(m, "corrupted")
if not exists (p in Posts where m in p.linked_media):
ensures: ValidationIssueReported(m, "orphan")
}
-- ============================================================================
-- MEDIA EXPORT/EXPORT RULES
-- ============================================================================
invariant MediaSidecarFormat {
-- Sidecar files use YAML frontmatter
-- Same structure as Media entity fields
-- Only truthy fields are written (except required fields)
}
-- ============================================================================
-- IMAGE OPTIMIZATION
-- ============================================================================
config {
-- Image optimization settings
max_original_size: Integer = 20971520 -- 20 MB in bytes
max_width: Integer = 4096
max_height: Integer = 4096
strip_exif: Boolean = true -- Strip EXIF from thumbnails
preserve_color_profile: Boolean = false -- Don't preserve ICC profiles
}
rule OptimizeMedia {
when: ImportMediaRequested(source_path, project)
requires: file_size(source_path) <= config.max_original_size
-- Original files are stored as-is (no compression)
-- Only thumbnails are optimized
@guidance
-- Media entity is created by ImportMedia rule
-- This rule documents the size constraint
}
-- ============================================================================
-- MEDIA SEARCH INDEXING
-- ============================================================================
rule IndexMediaForSearch {
when: SearchIndexUpdated(media: media/Media)
-- Index fields: title, alt, caption, original_name, tags
-- Plus all translation titles, alts, and captions
-- Note: media.translations not available in this context
let all_text = concat(
media.title,
media.alt,
media.caption,
media.original_name,
join(media.tags, " ")
)
let stemmed = stem(all_text, detect_language(all_text))
ensures: search/MediaSearchIndex.created(
media: media,
stemmed_content: stemmed
)
}

411
specs/schema.allium Normal file
View File

@@ -0,0 +1,411 @@
-- allium: 1
-- bDS SQLite Database Schema
-- Scope: core (Wave 1 — exact compatibility contract)
-- Distilled from: ../bDS/src/main/database/schema.ts
--
-- This document specifies the exact SQLite schema that the Rust
-- implementation must be able to read and write. It is the ground truth
-- for database compatibility.
-- ============================================================================
-- CORE ENTITIES
-- ============================================================================
entity Project {
id: String -- UUID v4
name: String -- Display name
slug: String -- URL-safe identifier
description: String? -- Optional description
data_path: String? -- Custom data directory (null = default)
created_at: Timestamp -- Unix timestamp
updated_at: Timestamp -- Unix timestamp
is_active: Boolean -- Exactly one project is active at a time
}
entity Post {
id: String -- UUID v4
project_id: String
title: String
slug: String -- URL-friendly identifier
excerpt: String? -- Optional summary
content: String? -- Draft body (null when published)
status: draft | published | archived
author: String? -- Author name
created_at: Timestamp
updated_at: Timestamp
published_at: Timestamp?
file_path: String -- Empty for never-published drafts
checksum: String? -- SHA-256 of content
tags: Set<String> -- JSON array stored as text
categories: Set<String> -- JSON array stored as text
template_slug: String? -- User template override
language: String? -- ISO 639-1 code
do_not_translate: Boolean
-- Legacy columns (read-only, no longer written)
published_title: String?
published_content: String?
published_tags: String?
published_categories: String?
published_excerpt: String?
}
entity PostTranslation {
id: String -- UUID v4
project_id: String
translation_for: String -- Canonical post ID
language: String -- ISO 639-1 code
title: String
excerpt: String?
content: String? -- Draft body (null when published)
status: draft | published | archived
created_at: Timestamp
updated_at: Timestamp
published_at: Timestamp?
file_path: String
checksum: String?
}
entity Media {
id: String -- UUID v4
project_id: String
filename: String -- Generated filename
original_name: String -- Original uploaded filename
mime_type: String -- e.g. "image/jpeg"
size: Integer -- Bytes
width: Integer? -- Image dimensions
height: Integer?
title: String?
alt: String?
caption: String?
author: String?
file_path: String -- Absolute path to binary
sidecar_path: String -- Path to .meta sidecar file
created_at: Timestamp
updated_at: Timestamp
checksum: String?
tags: Set<String> -- JSON array stored as text
language: String? -- ISO 639-1 code
}
entity MediaTranslation {
id: String -- UUID v4
project_id: String
translation_for: String -- Canonical media ID
language: String -- ISO 639-1 code
title: String?
alt: String?
caption: String?
created_at: Timestamp
updated_at: Timestamp
}
entity Tag {
id: String -- UUID v4
project_id: String
name: String -- Case-insensitive unique per project
color: String? -- Hex color like #ff0000
post_template_slug: String? -- Template override for this tag
created_at: Timestamp
updated_at: Timestamp
}
entity Template {
id: String -- UUID v4
project_id: String
slug: String -- URL-safe identifier
title: String
kind: post | list | not_found | partial
enabled: Boolean
version: Integer -- Incremented on each update
file_path: String -- templates/{slug}.liquid
status: draft | published
content: String? -- Draft body (null when published)
created_at: Timestamp
updated_at: Timestamp
}
entity Script {
id: String -- UUID v4
project_id: String
slug: String -- URL-safe identifier
title: String
kind: macro | utility | transform
entrypoint: String -- Default: "render" for macros
enabled: Boolean
version: Integer -- Incremented on each update
file_path: String -- scripts/{slug}.lua (Rust) / {slug}.py (TS)
status: draft | published
content: String? -- Draft body (null when published)
created_at: Timestamp
updated_at: Timestamp
}
-- ============================================================================
-- RELATIONSHIP TABLES
-- ============================================================================
entity PostLink {
id: String -- UUID v4
source_post_id: String -- Post containing the link
target_post_id: String -- Post being linked to
link_text: String? -- Anchor text
created_at: Timestamp
}
entity PostMediaLink {
id: String -- UUID v4
project_id: String
post_id: String
media_id: String
sort_order: Integer -- For ordering media within a post
created_at: Timestamp
}
-- ============================================================================
-- METADATA TABLES
-- ============================================================================
entity Setting {
key: String -- Primary key
value: String -- Serialized value
updated_at: Timestamp
}
entity GeneratedFileHash {
project_id: String
relative_path: String
content_hash: String -- SHA-256 of file content
updated_at: Timestamp
}
-- ============================================================================
-- SEARCH INDEX (FTS5 Virtual Tables)
-- ============================================================================
entity PostSearchIndex {
-- SQLite FTS5 virtual table, not a real entity
-- Created via: CREATE VIRTUAL TABLE posts_fts USING fts5(...)
-- Indexed fields: title, excerpt, content, tags, categories
-- Plus all translation titles, excerpts, and content
post: Post
stemmed_content: String -- Processed via Snowball stemmer
}
entity MediaSearchIndex {
-- SQLite FTS5 virtual table
-- Created via: CREATE VIRTUAL TABLE media_fts USING fts5(...)
-- Indexed fields: title, alt, caption, original_name, tags
-- Plus all translation titles, alts, and captions
media: Media
stemmed_content: String -- Processed via Snowball stemmer
}
-- ============================================================================
-- AI / CHAT TABLES
-- ============================================================================
entity ChatConversation {
id: String -- UUID v4
title: String
model: String? -- Model used for conversation
copilot_session_id: String? -- Legacy, no longer used
created_at: Timestamp
updated_at: Timestamp
}
entity ChatMessage {
id: Integer -- Auto-increment
conversation_id: String
role: system | user | assistant | tool
content: String?
tool_call_id: String? -- For tool responses
tool_calls: String? -- JSON array of tool calls
created_at: Timestamp
}
entity AiProvider {
id: String -- Provider key (e.g., "opencode", "mistral")
name: String -- Display name
env: String? -- JSON array of env var names
npm: String? -- Primary npm package
api: String? -- API base URL
doc: String? -- Documentation URL
updated_at: Timestamp
}
entity AiModel {
provider: String -- FK → ai_providers.id
model_id: String
name: String -- Display name
family: String? -- Model family (e.g., "claude-sonnet")
attachment: Boolean -- Supports attachments
reasoning: Boolean -- Supports reasoning
tool_call: Boolean -- Supports tool calls
structured_output: Boolean
temperature: Boolean -- Temperature control supported
knowledge: String? -- Knowledge cutoff date
release_date: String?
last_updated_date: String?
open_weights: Boolean
input_price: Integer? -- USD per 1M input tokens (in cents)
output_price: Integer? -- USD per 1M output tokens (in cents)
cache_read_price: Integer?
cache_write_price: Integer?
context_window: Integer -- Max context tokens
max_input_tokens: Integer
max_output_tokens: Integer
interleaved: String? -- JSON object for interleaved fields
status: String? -- e.g., "deprecated"
provider_npm: String? -- Per-model npm override
updated_at: Timestamp
}
entity AiModelModality {
provider: String
model_id: String
direction: String -- "input" | "output"
modality: String -- "text" | "image" | "pdf" | "audio" | "video"
}
entity AiCatalogMeta {
key: String -- "etag" | "lastFetchedAt"
value: String
}
-- ============================================================================
-- EMBEDDINGS TABLES
-- ============================================================================
entity EmbeddingKey {
label: Integer -- USearch bigint key
post_id: String
project_id: String
content_hash: String -- SHA-256 of title+content
vector: String -- Base64-encoded Float32Array bytes (1536 bytes for 384-dim)
}
entity DismissedDuplicatePair {
id: String -- UUID v4
project_id: String
post_id_a: String
post_id_b: String
dismissed_at: Timestamp
}
-- ============================================================================
-- IMPORT TABLES
-- ============================================================================
entity ImportDefinition {
id: String -- UUID v4
project_id: String
name: String
wxr_file_path: String? -- WordPress XML export file
uploads_folder_path: String? -- WordPress uploads directory
last_analysis_result: String? -- JSON text of ImportAnalysisReport
created_at: Timestamp
updated_at: Timestamp
}
-- ============================================================================
-- NOTIFICATION TABLES
-- ============================================================================
entity DbNotification {
id: Integer -- Auto-increment
entity_type: String -- 'post' | 'media' | 'script' | 'template'
entity_id: String
action: created | updated | deleted
from_cli: Boolean -- 1 = written by CLI
seen_at: Timestamp? -- NULL = unprocessed
created_at: Timestamp
}
-- ============================================================================
-- SCHEMA CONSTRAINTS AND INDEXES
-- ============================================================================
invariant UniqueProjectSlug {
-- projects.slug must be unique across all projects
}
invariant UniquePostSlugPerProject {
-- posts.slug must be unique within each project.project_id
-- Enforced by: posts_project_slug_idx unique index
}
invariant UniqueTranslationPerPostLanguage {
-- post_translations must have unique (translation_for, language)
-- Enforced by: post_translations_translation_language_idx
}
invariant UniqueMediaTranslationPerMediaLanguage {
-- media_translations must have unique (translation_for, language)
-- Enforced by: media_translations_translation_language_idx
}
invariant UniqueTagNamePerProject {
-- tags.name must be unique within each project.project_id
-- Enforced by: tags_project_name_idx unique index
}
invariant UniqueScriptSlugPerProject {
-- scripts.slug must be unique within each project.project_id
-- Enforced by: scripts_project_slug_idx unique index
}
invariant UniqueTemplateSlugPerProject {
-- templates.slug must be unique within each project.project_id
-- Enforced by: templates_project_slug_idx unique index
}
invariant UniquePostMediaLink {
-- post_media must have unique (post_id, media_id) pair
-- Enforced by: post_media_post_media_idx unique index
}
invariant UniqueGeneratedFileHash {
-- generated_file_hashes must have unique (project_id, relative_path)
-- Enforced by: generated_file_hashes_project_path_idx unique index
}
invariant UniqueDismissedDuplicatePair {
-- dismissed_duplicate_pairs must have unique (project_id, post_id_a, post_id_b)
-- Enforced by: dismissed_pairs_idx unique index
}
-- ============================================================================
-- FTS5 VIRTUAL TABLE SCHEMAS (Snowball Stemmer Integration)
-- ============================================================================
value Fts5PostSchema {
-- CREATE VIRTUAL TABLE posts_fts USING fts5(
-- title, excerpt, content, tags, categories,
-- content='posts',
-- content_rowid='rowid'
-- );
fields: Set<String> -- {title, excerpt, content, tags, categories}
stemmer_languages: Integer = 24
}
value Fts5MediaSchema {
-- CREATE VIRTUAL TABLE media_fts USING fts5(
-- title, alt, caption, original_name, tags,
-- content='media',
-- content_rowid='rowid'
-- );
fields: Set<String> -- {title, alt, caption, original_name, tags}
stemmer_languages: Integer = 24
}
-- ============================================================================
-- MIGRATION HISTORY
-- ============================================================================
value MigrationVersion {
-- Schema version tracking via refinery migrations
-- Current version: 0007 (scripts and templates draft lifecycle)
-- Migration files located in: migrations/
-- Note: Migration list documented in comments, not as Allium value
}

View File

@@ -0,0 +1,263 @@
-- allium: 1
-- bDS Liquid Template Context Specification
-- Scope: core (Wave 4 — rendering parity)
-- Distilled from: ../bDS/src/main/engine/GenerationRouteRendererFactory.ts,
-- PageRenderer.ts, BlogGenerationEngine.ts
--
-- This document specifies the exact data structure passed to Liquid templates
-- during rendering. It is the contract between the generation engine and
-- template authors.
-- ============================================================================
-- GLOBAL TEMPLATE VARIABLES
-- ============================================================================
value RenderContext {
-- Top-level variables available in all templates
language: String -- Current language code
language_prefix: String? -- "/de" or "" depending on language
html_theme_attribute: String -- Theme class for <html> element
page_title: String -- Page title for <title> tag
pico_stylesheet_href: String -- Path to Pico CSS theme
blog_languages: List<BlogLanguage>
alternate_links: List<AlternateLink>
menu_items: List<MenuItem>
calendar_initial_year: Integer
calendar_initial_month: Integer
post: PostContext? -- Present on single post pages
post_categories: List<Category>
post_tags: List<Tag>
tag_color_by_name: Map<String, String>
backlinks: List<Backlink>
day_blocks: List<DayBlock>
archive_context: ArchiveContext?
show_archive_range_heading: Boolean
min_date: Timestamp?
max_date: Timestamp?
is_list_page: Boolean
is_first_page: Boolean
is_last_page: Boolean
has_prev_page: Boolean
has_next_page: Boolean
prev_page_href: String?
next_page_href: String?
not_found_message: String?
not_found_back_label: String?
-- Lookup maps for macro expansion
canonical_post_path_by_slug: Map<String, String>
canonical_media_path_by_source_path: Map<String, String>
post_data_json_by_id: Map<String, PostDataJson>
}
value BlogLanguage {
is_current: Boolean
code: String
flag: String
href: String
href_prefix: String
}
value AlternateLink {
href: String
hreflang: String
}
value MenuItem {
href: String
title: String
has_children: Boolean
children: List<MenuItem>?
}
-- ============================================================================
-- POST CONTEXT (Single Post Pages)
-- ============================================================================
value PostContext {
id: String
title: String
content: String -- Rendered HTML (after markdown + macros)
slug: String
excerpt: String?
author: String?
language: String?
show_title: Boolean -- Always true for post pages
published_at: Timestamp
created_at: Timestamp
updated_at: Timestamp
tags: List<String>
categories: List<String>
template_slug: String?
do_not_translate: Boolean
linked_media: List<MediaContext>
outgoing_links: List<LinkContext>
incoming_links: List<LinkContext>
}
value PostDataJson {
-- Serialized post data for Liquid template access
id: String
title: String
slug: String
excerpt: String?
author: String?
language: String?
published_at: Timestamp
created_at: Timestamp
updated_at: Timestamp
tags: List<String>
categories: List<String>
}
value MediaContext {
id: String
filename: String
original_name: String
mime_type: String
title: String?
alt: String?
caption: String?
author: String?
width: Integer?
height: Integer?
file_path: String
}
value LinkContext {
href: String
title: String
display_slug: String
}
-- ============================================================================
-- ARCHIVE CONTEXT (List Pages)
-- ============================================================================
value ArchiveContext {
kind: category | tag | date
name: String?
month: Integer?
year: Integer?
day: Integer?
}
value DayBlock {
show_date_marker: Boolean
date_label: String
posts: List<PostContext>
show_separator: Boolean
}
value Category {
name: String
slug: String
post_count: Integer
}
value Tag {
name: String
slug: String
color: String?
post_count: Integer
}
value Backlink {
path: String
display_slug: String
title: String
}
-- ============================================================================
-- PAGINATION CONTEXT
-- ============================================================================
value PaginationContext {
is_list_page: Boolean
is_first_page: Boolean
is_last_page: Boolean
has_prev_page: Boolean
has_next_page: Boolean
prev_page_href: String?
next_page_href: String?
current_page: Integer
total_pages: Integer
total_items: Integer
items_per_page: Integer
}
-- ============================================================================
-- LIQUID FILTER SPECIFICATION
-- Note: Allium does not have a 'filter' keyword. Filters are documented here
-- for reference but are implemented in the Liquid engine, not in Allium specs.
-- ============================================================================
-- Built-in filters (from liquid-rust crate):
-- default: {{ value | default: "fallback" }}
-- escape: {{ text | escape }}
-- url_encode: {{ text | url_encode }}
-- append: {{ text | append: suffix }}
--
-- Custom filters (must be implemented in Rust Liquid engine):
-- i18n: {{ "key" | i18n: language }} - translation lookup
-- markdown: {{ content | markdown: ... }} - markdown rendering with macro expansion
-- ============================================================================
-- BUILT-IN MACROS
-- Note: Allium does not have a 'macro' keyword. Macros are documented here
-- for reference but are implemented as Rust functions in bds-core/render.
-- ============================================================================
-- Built-in macros (implemented as native Rust functions):
-- gallery: [[gallery images=post.linked_media columns=3]]
-- youtube: [[youtube id=dQw4w9WgXcQ]]
-- vimeo: [[vimeo id=123456789]]
-- photo_archive: [[photo_archive media=project.media]]
-- tag_cloud: [[tag_cloud tags=Tags]]
-- ============================================================================
-- TEMPLATE LOOKUP RULES
-- ============================================================================
invariant TemplateLookupPriority {
-- Templates are resolved in this order:
-- 1. Post-specific template (post.template_slug)
-- 2. Tag-specific template (tag.post_template_slug)
-- 3. Category-specific template (category.post_template_slug)
-- 4. Default "post" template
--
-- List pages use "list" template
-- 404 pages use "not-found" template
-- Partials are referenced via {% render 'partial' %}
}
invariant PartialResolution {
-- Partials are looked up in templates/ directory
-- Usage: {% render 'partials/header', context_var: value %}
-- Partial files: templates/partials/{name}.liquid
}
-- ============================================================================
-- RENDERING RULES
-- ============================================================================
rule RenderPostPage {
when: RenderPostPageRequested(post, language)
let template = resolve_template(post)
let context = BuildPostContext(post, language)
ensures: RenderedHtml = liquid_render(template.content, context)
}
rule RenderListPage {
when: RenderListPageRequested(posts, pagination, archive_context)
let template = first (t in Templates where t.kind = "list" and t.enabled)
let context = BuildListContext(posts, pagination, archive_context)
ensures: RenderedHtml = liquid_render(template.content, context)
}
rule RenderNotFoundPage {
when: RenderNotFoundPageRequested()
let template = first (t in Templates where t.kind = "not-found" and t.enabled)
let context = BuildNotFoundContext
ensures: RenderedHtml = liquid_render(template.content, context)
}