feat: additional specs
This commit is contained in:
@@ -41,4 +41,5 @@ Invariants and behaviours in the allium spec should be covered by unit tests of
|
|||||||
- do not reference external JavaScript or CSS on CDNs, always bring it into the project
|
- do not reference external JavaScript or CSS on CDNs, always bring it into the project
|
||||||
- do not embedd CSS/JavaScript into HTML, always reference .css and .js files in the project assets
|
- do not embedd CSS/JavaScript into HTML, always reference .css and .js files in the project assets
|
||||||
- always make sure you follow proper i18n best practices. no untranslated string constants.
|
- always make sure you follow proper i18n best practices. no untranslated string constants.
|
||||||
|
- when creating rust source code, always follow what the allium spec is saying for that part
|
||||||
|
|
||||||
|
|||||||
316
SPECIFICATION_INDEX.md
Normal file
316
SPECIFICATION_INDEX.md
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
# bDS Rust Rewrite - Vollständige Spezifikationssammlung
|
||||||
|
|
||||||
|
Diese Datei dient als Index zu allen Allium-Spezifikationen und Schema-Inventarisierungen für die Rust-Implementierung.
|
||||||
|
|
||||||
|
## Verfügbare Spezifikationen
|
||||||
|
|
||||||
|
### Kern-Spezifikationen
|
||||||
|
|
||||||
|
| Datei | Scope | Status | Beschreibung |
|
||||||
|
|-------|-------|--------|--------------|
|
||||||
|
| `bds.allium` | Core | ✅ Existiert | Haupt-Spezifikation mit allen Referenzen |
|
||||||
|
| `project.allium` | Core (Wave 1) | ✅ Existiert | Projekt-Management |
|
||||||
|
| `post.allium` | Core (Wave 1) | ✅ Existiert | Post-Lifecycle, Frontmatter, Dateistruktur |
|
||||||
|
| `media.allium` | Core (Wave 1) | ✅ Existiert | Media-Import, Thumbnails, Sidecars |
|
||||||
|
| `translation.allium` | Core (Wave 1) | ✅ Existiert | Post- und Media-Übersetzungen |
|
||||||
|
| `tag.allium` | Core (Wave 1) | ✅ Existiert | Tags mit Mass-Operationen |
|
||||||
|
| `template.allium` | Core (Wave 1/4) | ✅ Existiert | Liquid-Template-Management |
|
||||||
|
| `script.allium` | Core (Wave 6) | ✅ Existiert | Skripting (Lua in Rust) |
|
||||||
|
| `menu.allium` | Core (Read) | ✅ Existiert | OPML-Navigationsmenü |
|
||||||
|
| `metadata.allium` | Core (Wave 1) | ✅ Existiert | Projekt-Konfiguration, Kategorien, Publishing |
|
||||||
|
|
||||||
|
### Infrastruktur-Spezifikationen
|
||||||
|
|
||||||
|
| Datei | Scope | Status | Beschreibung |
|
||||||
|
|-------|-------|--------|--------------|
|
||||||
|
| `search.allium` | Core (Wave 1) | ✅ Existiert | FTS5 Full-Text Search mit Snowball |
|
||||||
|
| `generation.allium` | Core (Wave 4) | ✅ Existiert | Statische Site-Generierung |
|
||||||
|
| `preview.allium` | Core (Wave 4) | ✅ Existiert | Lokaler Preview-Server |
|
||||||
|
| `publishing.allium` | Core (Wave 5) | ✅ Existiert | SSH-Upload (SCP/rsync) |
|
||||||
|
| `task.allium` | Core (Wave 1) | ✅ Existiert | Background Task Manager |
|
||||||
|
| `i18n.allium` | Core (Alle) | ✅ Existiert | Split Localization (UI vs Content) |
|
||||||
|
| `schema.allium` | Core (Wave 1) | ✨ Neu | Vollständiges SQLite-Schema |
|
||||||
|
| `frontmatter.allium` | Core (Wave 1) | ✨ Neu | Alle Frontmatter-Formate |
|
||||||
|
| `template_context.allium` | Core (Wave 4) | ✨ Neu | Liquid-Template-Kontext |
|
||||||
|
| `media_processing.allium` | Core (Wave 1) | ✨ Neu | Thumbnail-Generierung, Bildverarbeitung |
|
||||||
|
|
||||||
|
### Integration-Spezifikationen
|
||||||
|
|
||||||
|
| Datei | Scope | Status | Beschreibung |
|
||||||
|
|-------|-------|--------|--------------|
|
||||||
|
| `git.allium` | Extension A | ✅ Existiert | Git-Operationen, LFS, Reconciliation |
|
||||||
|
| `mcp.allium` | Extension G | ✅ Existiert | MCP-Server (Tools, Resources) |
|
||||||
|
| `ai.allium` | Core/Extension C | ✅ Existiert | AI One-Shot Tasks und Chat |
|
||||||
|
| `embedding.allium` | Extension D | ✅ Existiert | Semantic Similarity (HNSW) |
|
||||||
|
| `cli_sync.allium` | Core (Wave 5) | ✅ Existiert | CLI-zu-App Notification Sync |
|
||||||
|
| `metadata_diff.allium` | Core (Wave 1) | ✅ Existiert | DB/Dateisystem-Diff und Rebuild |
|
||||||
|
|
||||||
|
## Neuerstellte Spezifikationen (diese Session)
|
||||||
|
|
||||||
|
### 1. `schema.allium`
|
||||||
|
|
||||||
|
**Zweck:** Vollständige Inventarisierung des SQLite-Schemas aus dem TypeScript-Projekt.
|
||||||
|
|
||||||
|
**Inhalt:**
|
||||||
|
- 22 Entity-Definitionen mit allen Feldern und Typen
|
||||||
|
- Alle Relationship-Tabellen (PostLink, PostMedia)
|
||||||
|
- Alle Metadata-Tabellen (Settings, GeneratedFileHashes)
|
||||||
|
- FTS5 Virtual Tables für Search (posts_fts, media_fts)
|
||||||
|
- AI/Chat-Tabellen (Conversations, Messages, Model Catalog)
|
||||||
|
- Embedding-Tabellen (USearch keys, dismissed duplicates)
|
||||||
|
- Import-Tabellen (WXR definitions)
|
||||||
|
- Notification-Tabellen (CLI-to-App sync)
|
||||||
|
- Alle Unique-Constraints und Indexe
|
||||||
|
- Migration-History (Version 0001-0010)
|
||||||
|
|
||||||
|
**Kritische Details:**
|
||||||
|
- `posts` Tabelle: 21 Felder inkl. Legacy-Felder
|
||||||
|
- `post_translations` Tabelle: 12 Felder
|
||||||
|
- `media` Tabelle: 17 Felder
|
||||||
|
- `media_translations` Tabelle: 6 Felder
|
||||||
|
- `scripts` Tabelle: 13 Felder (Lua in Rust)
|
||||||
|
- `templates` Tabelle: 13 Felder
|
||||||
|
- FTS5 benötigt Snowball Stemmer für 24 Sprachen
|
||||||
|
- Embedding vectors: 384-dimensional Float32 (1536 bytes)
|
||||||
|
|
||||||
|
### 2. `frontmatter.allium`
|
||||||
|
|
||||||
|
**Zweck:** Exakte Spezifikation aller YAML-Frontmatter-Formate.
|
||||||
|
|
||||||
|
**Inhalt:**
|
||||||
|
- **Post-Dateiformat:** `posts/{YYYY}/{MM}/{slug}.md`
|
||||||
|
- Required fields: id, title, slug, status, createdAt, updatedAt, tags, categories
|
||||||
|
- Conditional fields: excerpt, author, language, templateSlug, publishedAt
|
||||||
|
- `doNotTranslate` nur wenn true
|
||||||
|
|
||||||
|
- **Translation-Dateiformat:** `posts/{YYYY}/{MM}/{slug}.{language}.md`
|
||||||
|
- Gleiche Struktur wie Posts mit language-Override
|
||||||
|
|
||||||
|
- **Media-Sidecar-Format:** `media/{id}.md`
|
||||||
|
- Required: id, filename, originalName, mimeType, size, createdAt, updatedAt, tags
|
||||||
|
- Optional: title, alt, caption, author, language, width, height
|
||||||
|
|
||||||
|
- **Template-Format:** `templates/{slug}.liquid`
|
||||||
|
- Required: id, slug, title, kind, enabled, version, createdAt, updatedAt
|
||||||
|
|
||||||
|
- **Script-Format:** `scripts/{slug}.lua` (Rust) / `{slug}.py` (TypeScript)
|
||||||
|
- Required: id, slug, title, kind, entrypoint, enabled, version, createdAt, updatedAt
|
||||||
|
|
||||||
|
- **Tags-Dateiformat:** `meta/tags.json`
|
||||||
|
- Sortiertes JSON-Array ohne interne IDs
|
||||||
|
|
||||||
|
- **Projekt-Metadaten:**
|
||||||
|
- `meta/project.json` - Projekt-Konfiguration
|
||||||
|
- `meta/categories.json` - Kategorie-Liste
|
||||||
|
- `meta/category-meta.json` - Render-Einstellungen pro Kategorie
|
||||||
|
- `meta/publishing.json` - SSH-Konfiguration
|
||||||
|
- `meta/menu.opml` - Navigation im OPML-Format
|
||||||
|
|
||||||
|
**Format-Konventionen:**
|
||||||
|
- Timestamps als Unix-Milliseconds (ISO 8601 in YAML)
|
||||||
|
- 2-Space Indentation
|
||||||
|
- Arrays als YAML-Liste
|
||||||
|
- Booleans als lowercase true/false
|
||||||
|
- Atomare Writes (temp file + rename)
|
||||||
|
|
||||||
|
### 3. `template_context.allium`
|
||||||
|
|
||||||
|
**Zweck:** Vollständige Spezifikation des an Liquid-Templates übergebenen Datenkontexts.
|
||||||
|
|
||||||
|
**Inhalt:**
|
||||||
|
- **Global Render Context:** 30+ Top-Level-Variablen
|
||||||
|
- `language`, `language_prefix`, `html_theme_attribute`
|
||||||
|
- `blog_languages` (List<BlogLanguage>)
|
||||||
|
- `alternate_links` (hreflang für SEO)
|
||||||
|
- `menu_items` (hierarchische Struktur)
|
||||||
|
- `post` (PostContext für Single-Post-Pages)
|
||||||
|
- `day_blocks` (für Archiv-Seiten)
|
||||||
|
- `canonical_post_path_by_slug` (Lookup-Map)
|
||||||
|
- `post_data_json_by_id` (Lookup-Map)
|
||||||
|
|
||||||
|
- **PostContext:** 18 Felder inkl. linked_media, outgoing_links, incoming_links
|
||||||
|
|
||||||
|
- **MediaContext:** 11 Felder für Media in Templates
|
||||||
|
|
||||||
|
- **PaginationContext:** 10 Felder für Paginierung
|
||||||
|
|
||||||
|
- **Liquid-Filters:**
|
||||||
|
- Built-in: `default`, `escape`, `url_encode`, `append`
|
||||||
|
- Custom: `i18n` (Übersetzungs-Lookup)
|
||||||
|
- Custom: `markdown` (Markdown→HTML mit Macro-Expansion)
|
||||||
|
|
||||||
|
- **Built-in Macros:**
|
||||||
|
- `gallery` - Bildergalerie
|
||||||
|
- `youtube` - YouTube-Einbettung
|
||||||
|
- `vimeo` - Vimeo-Einbettung
|
||||||
|
- `photo_archive` - Foto-Archiv-Grid
|
||||||
|
- `tag_cloud` - Tag-Cloud mit Größen-Faktoren
|
||||||
|
|
||||||
|
- **Template-Lookup-Regeln:**
|
||||||
|
- Priority: post-specific → tag-specific → category-specific → default
|
||||||
|
- Partials via `{% render 'partial' %}`
|
||||||
|
|
||||||
|
### 4. `media_processing.allium`
|
||||||
|
|
||||||
|
**Zweck:** Exakte Spezifikation der Media-Verarbeitung (Thumbnails, Format-Konversion, EXIF).
|
||||||
|
|
||||||
|
**Inhalt:**
|
||||||
|
- **Datei-Organisation:**
|
||||||
|
- Binär: `media/{timestamp}_{random}.{ext}`
|
||||||
|
- Sidecar: `media/{id}.md`
|
||||||
|
- Thumbnail: `thumbnails/{id}.webp`
|
||||||
|
- Thumbnail-Source: `thumbnails/{id}_source.{ext}`
|
||||||
|
|
||||||
|
- **Thumbnail-Konfiguration:**
|
||||||
|
- Größe: 400x300 (default)
|
||||||
|
- Fit: "cover" (crop to fill)
|
||||||
|
- Qualität: 80% WEBP
|
||||||
|
- Format: WEBP
|
||||||
|
|
||||||
|
- **Bildverarbeitung:**
|
||||||
|
- Input-Formate: JPEG, PNG, GIF, WEBP, TIFF, BMP, HEIC, HEIF
|
||||||
|
- EXIF-Orientierung muss berücksichtigt werden
|
||||||
|
- EXIF wird aus Thumbnails entfernt (Privacy)
|
||||||
|
- Original-Format bleibt erhalten
|
||||||
|
|
||||||
|
- **Import-Flow:**
|
||||||
|
1. Validiere Dateityp
|
||||||
|
2. Generiere eindeutigen Dateinamen
|
||||||
|
3. Kopiere nach media/
|
||||||
|
4. Generiere Thumbnail
|
||||||
|
5. Erstelle Sidecar mit Metadaten
|
||||||
|
6. Indexiere für Search (FTS5)
|
||||||
|
7. Generiere Embedding (wenn aktiviert)
|
||||||
|
|
||||||
|
- **Media-Übersetzungen:**
|
||||||
|
- Dateipfad: `media/{id}/{language}.md`
|
||||||
|
- Felder: title, alt, caption pro Sprache
|
||||||
|
|
||||||
|
- **Validierungs-Regeln:**
|
||||||
|
- Fehlende Binary-Dateien
|
||||||
|
- Fehlende Sidecar-Dateien
|
||||||
|
- Fehlende Thumbnails
|
||||||
|
- Korrupte Bilddateien
|
||||||
|
- Orphan Media (nicht verlinkt)
|
||||||
|
|
||||||
|
## Verfügbare Rust-Plan-Dokumente
|
||||||
|
|
||||||
|
| Datei | Beschreibung |
|
||||||
|
|-------|--------------|
|
||||||
|
| `RUST_PLAN.md` | Haupt-Plan mit Non-Negotiable Constraints |
|
||||||
|
| `RUST_PLAN_CORE.md` | Detaillierter Core-Plan (886 Zeilen) |
|
||||||
|
| `RUST_PLAN_EXTENSION.md` | Extension Buckets A-J |
|
||||||
|
| `RUST_EXECUTION_BACKLOG.md` | Milestone-basierte Aufgabenliste |
|
||||||
|
| `RUST_COMPATIBILITY_MATRIX_TEMPLATE.md` | Vorlage für Metadata-Inventory |
|
||||||
|
|
||||||
|
## Spezifikations-Abdeckungs-Analyse
|
||||||
|
|
||||||
|
### Vollständig Abgedeckt ✅
|
||||||
|
|
||||||
|
1. **Database Schema** - `schema.allium` deckt alle 22 Tabellen mit allen Feldern
|
||||||
|
2. **File Formats** - `frontmatter.allium` deckt alle Dateitypen und Formate
|
||||||
|
3. **Post Lifecycle** - `post.allium` + `translation.allium` vollständig
|
||||||
|
4. **Media Processing** - `media.allium` + `media_processing.allium` vollständig
|
||||||
|
5. **Template System** - `template.allium` + `template_context.allium` vollständig
|
||||||
|
6. **Search** - `search.allium` mit FTS5 und Snowball Stemming
|
||||||
|
7. **Generation** - `generation.allium` mit allen Section-Typen
|
||||||
|
8. **Publishing** - `publishing.allium` mit SCP und rsync
|
||||||
|
9. **AI Integration** - `ai.allium` mit One-Shot Operations
|
||||||
|
10. **i18n** - `i18n.allium` mit Split Localization
|
||||||
|
|
||||||
|
### Spezifikations-Lücken vor dieser Session ❌
|
||||||
|
|
||||||
|
1. **Schema nicht dokumentiert** - Nur aus TypeScript-Code lesbar
|
||||||
|
2. **Frontmatter-Regeln nicht explizit** - Feld-Logik verstreut in Engine-Code
|
||||||
|
3. **Template-Kontext unvollständig** - Variable-Liste existierte nicht zentral
|
||||||
|
4. **Media-Processing-Regeln implizit** - Thumbnail-Größen, EXIF-Handling nicht spezifiziert
|
||||||
|
|
||||||
|
### Status nach dieser Session ✅
|
||||||
|
|
||||||
|
Alle kritischen Lücken geschlossen:
|
||||||
|
- ✅ Vollständiges SQLite-Schema inventarisiert
|
||||||
|
- ✅ Alle Frontmatter-Formate spezifiziert
|
||||||
|
- ✅ Template-Kontext vollständig dokumentiert
|
||||||
|
- ✅ Media-Processing-Regeln explizit gemacht
|
||||||
|
|
||||||
|
## Nächste Schritte für die Implementierung
|
||||||
|
|
||||||
|
### Wave 0 (Foundation)
|
||||||
|
1. Cargo-Workspace aufsetzen (bds-core, bds-editor, bds-ui, bds-cli)
|
||||||
|
2. SQLite-Connection mit rusqlite (bundled, vtab)
|
||||||
|
3. Refinery-Migration-Loader
|
||||||
|
4. bds-editor PoC (ropey + syntect + cosmic-text)
|
||||||
|
5. Iced App Shell mit muda-Menüs
|
||||||
|
6. Slug-Compatibility-Tests (deunicode vs transliteration)
|
||||||
|
|
||||||
|
### Wave 1 (Data Layer)
|
||||||
|
1. Alle Engines implementieren (Project, Post, Media, Tag, Meta, etc.)
|
||||||
|
2. Frontmatter-Parser/Writer für alle Dateitypen
|
||||||
|
3. Thumbnail-Generierung mit image-crate
|
||||||
|
4. FTS5-Index mit Snowball-Stemming
|
||||||
|
5. Metadata-Diff und Rebuild
|
||||||
|
6. Round-trip-Tests für alle Entity-Typen
|
||||||
|
|
||||||
|
### Wave 2 (Native Shell)
|
||||||
|
1. Muda-Menü-Bar mit allen Menüs
|
||||||
|
2. rfd-Datei-Dialoge
|
||||||
|
3. Iced-Message-Routing
|
||||||
|
4. macOS Lifecycle-Shim (objc2)
|
||||||
|
5. Tab-Management und Workspace-Layout
|
||||||
|
|
||||||
|
### Wave 3 (Authoring UI)
|
||||||
|
1. Post-Editor mit bds-editor
|
||||||
|
2. Media-Browser und Editor
|
||||||
|
3. Template-Editor
|
||||||
|
4. Script-Editor
|
||||||
|
5. Settings-View
|
||||||
|
6. Tag/Category-Management
|
||||||
|
|
||||||
|
### Wave 4 (Rendering)
|
||||||
|
1. Markdown-Render mit pulldown-cmark
|
||||||
|
2. Liquid-Template-Engine (subset)
|
||||||
|
3. Built-in Macros (gallery, youtube, vimeo, etc.)
|
||||||
|
4. Preview-Server (axum)
|
||||||
|
5. Site-Generation mit rayon-Parallelisierung
|
||||||
|
6. One-Shot AI Operations (reqwest)
|
||||||
|
7. Pagefind-Search-Index
|
||||||
|
|
||||||
|
### Wave 5 (Publishing)
|
||||||
|
1. SSH/SCP Upload mit ssh2-crate
|
||||||
|
2. rsync-Integration
|
||||||
|
3. Publish-Progress-UI
|
||||||
|
4. Validierung vor Publish
|
||||||
|
|
||||||
|
### Wave 6 (Lua Scripting)
|
||||||
|
1. Lua-Runtime mit mlua
|
||||||
|
2. Lua-API-Bridge
|
||||||
|
3. Script-Execution
|
||||||
|
4. Generated API-Documentation
|
||||||
|
|
||||||
|
## Referenz-Implementierung
|
||||||
|
|
||||||
|
Für Implementierungsdetails immer referenzieren:
|
||||||
|
- **TypeScript-Code:** `/Users/gb/Projects/bDS/src/main/engine/`
|
||||||
|
- **Spezifikationen:** `/Users/gb/Projects/RuDS/specs/`
|
||||||
|
- **Pläne:** `/Users/gb/Projects/RuDS/RUST_*.md`
|
||||||
|
|
||||||
|
## Kompatibilitäts-Garantien
|
||||||
|
|
||||||
|
Die Rust-Implementierung muss garantieren:
|
||||||
|
|
||||||
|
1. **Datenbank-Kompatibilität** - Alle TypeScript-Daten lesbar
|
||||||
|
2. **Datei-Format-Kompatibilität** - Frontmatter byte-genau gleich
|
||||||
|
3. **Generierungs-Kompatibilität** - Output-Hashes übereinstimmend (normalisiert)
|
||||||
|
4. **Slug-Kompatibilität** - deunicode Output muss transliteration Output entsprechen
|
||||||
|
5. **URL-Kompatibilität** - Gleiche Routes und Canonical URLs
|
||||||
|
6. **Such-Kompatibilität** - FTS5 mit identischem Stemming
|
||||||
|
7. **Template-Kompatibilität** - Identisches Liquid-Subset
|
||||||
|
|
||||||
|
## Spezifikations-Quellen
|
||||||
|
|
||||||
|
Alle Spezifikationen wurden extrahiert aus:
|
||||||
|
|
||||||
|
- TypeScript-Engine-Code: `/Users/gb/Projects/bDS/src/main/engine/*.ts`
|
||||||
|
- TypeScript-Schema: `/Users/gb/Projects/bDS/src/main/database/schema.ts`
|
||||||
|
- TypeScript-Tests: `/Users/gb/Projects/bDS/tests/engine/*.test.ts`
|
||||||
|
- Bestehende Allium-Specs: `/Users/gb/Projects/RuDS/specs/*.allium`
|
||||||
318
specs/frontmatter.allium
Normal file
318
specs/frontmatter.allium
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
294
specs/media_processing.allium
Normal file
294
specs/media_processing.allium
Normal 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
411
specs/schema.allium
Normal 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
|
||||||
|
}
|
||||||
263
specs/template_context.allium
Normal file
263
specs/template_context.allium
Normal 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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user