chore: more complete spec and more aligned with plan

This commit is contained in:
2026-04-05 10:33:49 +02:00
parent dafe7a5357
commit 7c6b19af07
9 changed files with 58 additions and 97 deletions

View File

@@ -20,11 +20,11 @@ surface UserAction {
-- ─── AI operation gating ───────────────────────────────
invariant AIOperationGating {
-- All AI operations check airplane/offline mode before executing.
-- If offline mode enabled:
-- If local provider configured (Ollama/LM Studio): route to local model
-- Else: show toast "AI unavailable in offline mode", abort operation
-- If online: use configured provider endpoint
-- All AI operations route through the active endpoint for the current mode.
-- See ai.allium AirplaneModeGating for endpoint selection logic.
-- If airplane_mode: use airplane endpoint (local model, e.g. Ollama/LM Studio)
-- If online: use online endpoint (cloud provider)
-- If active endpoint not configured: show toast, abort operation
-- Two model categories:
-- Title model: text analysis (suggestions, translation, language detect)
-- Image model: vision-capable (image analysis only)

View File

@@ -46,49 +46,47 @@ entity ChatMessage {
-- One-shot AI tasks (core scope, no streaming)
-- All use OpenAI Chat Completions wire format.
-- When airplane_mode: use airplane endpoint. When online: use online endpoint.
-- Endpoint routing: see AirplaneModeGating invariant below.
-- When no endpoint configured for current mode: disable AI, show toast.
rule AnalyzeTaxonomy {
when: AnalyzeTaxonomyRequested(post)
requires: not airplane_mode
-- All AI activities gated by offline mode
requires: active_endpoint_configured
-- Suggests tags and categories for a post
ensures: TaxonomySuggestion(tags, categories)
}
rule AnalyzeImage {
when: AnalyzeImageRequested(media)
requires: not airplane_mode
requires: active_endpoint_configured
requires: is_image(media.mime_type)
-- Checks mime type is an image type
-- Vision model generates alt text and caption
ensures: ImageAnalysisResult(alt, caption)
}
rule AnalyzePost {
when: AnalyzePostRequested(post)
requires: not airplane_mode
requires: active_endpoint_configured
-- Generates title, excerpt, slug suggestions
ensures: PostAnalysisResult(title, excerpt, slug)
}
rule DetectLanguage {
when: DetectLanguageRequested(text)
requires: not airplane_mode
requires: active_endpoint_configured
ensures: LanguageDetectionResult(language_code)
}
rule TranslatePost {
when: TranslatePostRequested(post, target_language)
requires: not airplane_mode
requires: active_endpoint_configured
-- Translates title, excerpt, content to target language
ensures: TranslationResult(title, excerpt, content)
}
rule TranslateMedia {
when: TranslateMediaRequested(media, target_language)
requires: not airplane_mode
requires: active_endpoint_configured
-- Translates title, alt, caption to target language
ensures: MediaTranslationResult(title, alt, caption)
}
@@ -102,7 +100,7 @@ rule StartChat {
rule SendChatMessage {
when: SendChatMessageRequested(conversation, content)
requires: not airplane_mode
requires: active_endpoint_configured
ensures: ChatMessage.created(conversation: conversation, role: user, content: content)
ensures: AiStreamingResponse(conversation)
-- AI SDK v6 streamText() with tool-call loop (max 10 rounds)
@@ -120,11 +118,13 @@ rule RefreshModelCatalog {
}
invariant AirplaneModeGating {
-- All AI activities must be gated by airplane (offline) mode
-- When airplane_mode = true: only the airplane endpoint is used
-- When airplane_mode = false: the online endpoint is used
-- If the active endpoint is not configured: AI is unavailable,
-- show toast notification to user
-- Endpoint routing based on airplane (offline) mode:
-- airplane_mode = true -> use airplane endpoint (local model)
-- airplane_mode = false -> use online endpoint (cloud provider)
-- active_endpoint_configured = true iff the endpoint for the
-- current mode has a non-empty url (and api_key for online).
-- When active endpoint is not configured: AI is unavailable,
-- show toast "AI unavailable — configure {online|airplane} endpoint in Settings"
}
invariant TwoEndpointModel {

View File

@@ -63,7 +63,7 @@ use "./metadata_diff.allium" as metadata_diff -- DB/filesystem diff and rebuil
--
-- MAY change intentionally:
-- Implementation language (TS -> Rust), editor (WYSIWYG -> plain text + preview),
-- scripting runtime (Python -> Lua), process model (Electron -> native),
-- scripting runtime (Lua), process model (Electron -> native),
-- UI framework (React -> Iced)
-- Resolved questions:

View File

@@ -37,18 +37,18 @@ surface User {
-- Add Category: text input + Add button. Reset to Defaults button
-- 4. AI Assistant Settings:
-- API keys: OpenCode, Mistral (masked display + Change, or password input + Save)
-- Local providers: Ollama enable checkbox + capabilities table,
-- LM Studio enable checkbox + capabilities table
-- Offline mode: enable checkbox (disabled if no local providers),
-- selectors for chat/title/image models (local only)
-- Default Model (select with optgroup by provider + refresh),
-- shows: max output tokens, context window, pricing
-- Title Model (select), Image Analysis Model (vision-capable only)
-- Two-endpoint configuration (see ai.allium TwoEndpointModel):
-- Online endpoint: URL (text), API Key (masked + Change/Save), Model (select)
-- Airplane endpoint: URL (text), Model (select)
-- Both use OpenAI Chat Completions wire format.
-- Refresh Models button per endpoint: fetches model list from endpoint URL
-- Title Model (select from active endpoint models)
-- Image Analysis Model (select, vision-capable only)
-- Offline mode toggle: switches between online and airplane endpoint
-- Disabled if airplane endpoint URL is not configured
-- System Prompt (textarea 12 rows + Save + Reset to Default)
-- 5. Technology Settings:
-- Python Runtime Mode (select: Web Worker/Main Thread)
-- Semantic Similarity (checkbox)
-- 6. Publishing Settings (SSH):
@@ -69,20 +69,15 @@ surface User {
-- ─── Settings view actions ──────────────────────────────────
-- API key validation:
-- On Save: test API call to provider endpoint before persisting
-- On Save: test API call to online endpoint before persisting
-- On validation failure: toast error message, key not saved
-- On success: key saved, masked display shown
-- On success: key encrypted via SecureKeyStore, masked display shown
-- Local provider enable/disable:
-- Enabling Ollama/LM Studio: fetches model list, shows capabilities table
-- Capabilities table columns: model name, chat, title, image (checkmarks)
-- Models auto-fetched on enable; manual refresh via Refresh Models button
-- Model catalog refresh:
-- Refresh button next to Default Model selector
-- Fetches available models from all enabled providers
-- Updates optgroup dropdown with provider groupings
-- Per-model info: max output tokens, context window, pricing
-- Endpoint configuration:
-- URL field required for both endpoints
-- Refresh Models button fetches model list from endpoint URL
-- Model selector populated from fetched model list
-- Per-model info: max output tokens, context window (when available)
-- Rebuild operations (Data Maintenance section):
-- 6 buttons, each executes immediately (no confirmation dialog)

View File

@@ -100,7 +100,7 @@ value TemplateFrontmatter {
id: String -- UUID v4
slug: String
title: String
kind: post | list | not-found | partial
kind: post | list | not_found | partial
enabled: Boolean
version: Integer
created_at: Timestamp
@@ -122,9 +122,8 @@ rule WriteTemplateFile {
-- ============================================================================
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
-- File path: scripts/{slug}.lua
-- YAML frontmatter delimited by --- markers
id: String -- UUID v4
slug: String
title: String
@@ -261,9 +260,10 @@ invariant MenuOpmlFormat {
-- ============================================================================
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
-- Database: Unix milliseconds stored as INTEGER columns
-- YAML frontmatter: ISO 8601 strings (e.g. 2024-03-15T14:30:00.000Z)
-- Conversion on read: parse ISO 8601 → Unix ms
-- Conversion on write: Unix ms → ISO 8601
}
invariant YamlFormatting {

View File

@@ -88,7 +88,8 @@ rule ToggleAssistantSidebar {
value WindowTitleBar {
-- Platform-adaptive
-- menu_bar: rendered only on non-Mac platforms (5 groups: File, Edit, View, Blog, Help)
-- menu_bar: rendered only on non-Mac platforms (6 groups: App, File, Edit, View, Window, Help)
-- macOS: native menu bar via muda crate (same 6 groups)
-- Keyboard: Alt opens mnemonics, Alt+letter opens group
-- Arrow keys navigate groups and items, Enter/Space activates
-- View group hides devTools toggle when not in dev mode

View File

@@ -134,7 +134,7 @@ entity Script {
entrypoint: String -- Default: "render" for macros
enabled: Boolean
version: Integer -- Incremented on each update
file_path: String -- scripts/{slug}.lua (Rust) / {slug}.py (TS)
file_path: String -- scripts/{slug}.lua
status: draft | published
content: String? -- Draft body (null when published)
created_at: Timestamp
@@ -224,52 +224,19 @@ entity ChatMessage {
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
entity AiCachedModel {
-- Cached model list fetched from endpoint /v1/models API.
-- Keyed by endpoint kind so online and airplane caches are separate.
endpoint_kind: String -- "online" | "airplane"
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
context_window: Integer?
max_output_tokens: Integer?
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"
key: String -- "{endpoint_kind}_etag" | "{endpoint_kind}_lastFetchedAt"
value: String
}

View File

@@ -2,8 +2,7 @@
-- bDS Scripting System
-- Scope: core (Wave 6 — Lua runtime and scripting docs)
-- Distilled from: src/main/engine/ScriptEngine.ts, schema.ts
-- Note: TypeScript app uses Python/Pyodide. Rust rewrite uses Lua.
-- Behavioural contract is identical; only runtime changes.
-- Scripting runtime is Lua (mlua crate). Behavioural contract preserved.
entity Script {
slug: String
@@ -37,8 +36,7 @@ invariant ScriptFileLayout {
for s in Scripts where file_path != "":
s.file_path = format("scripts/{slug}.lua", slug: s.slug)
}
-- TypeScript app uses .py with triple-quote docstring frontmatter
-- Rust app uses .lua with standard --- frontmatter
-- Script files use .lua with standard --- YAML frontmatter
rule CreateScript {
when: CreateScriptRequested(title, kind, content, entrypoint)

View File

@@ -227,7 +227,7 @@ invariant TemplateLookupPriority {
-- 4. Default "post" template
--
-- List pages use "list" template
-- 404 pages use "not-found" template
-- 404 pages use "not_found" template
-- Partials are referenced via {% render 'partial' %}
}
@@ -257,7 +257,7 @@ rule RenderListPage {
rule RenderNotFoundPage {
when: RenderNotFoundPageRequested()
let template = first (t in Templates where t.kind = "not-found" and t.enabled)
let template = first (t in Templates where t.kind = "not_found" and t.enabled)
let context = BuildNotFoundContext
ensures: RenderedHtml = liquid_render(template.content, context)
}