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