initial commit

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-23 10:42:27 +02:00
commit cd998f24a9
57 changed files with 9751 additions and 0 deletions

118
specs/search.allium Normal file
View File

@@ -0,0 +1,118 @@
-- allium: 1
-- bDS Full-Text Search
-- Scope: core (Wave 1 — in-app full-text search with Snowball stemmers)
-- Distilled from: src/main/engine/PostEngine.ts (FTS methods),
-- MediaEngine.ts (FTS methods), stemmer.ts
use "./post.allium" as post
use "./media.allium" as media
surface SearchControlSurface {
facing _: SearchOperator
provides:
SearchPostsRequested(query, filters)
SearchMediaRequested(query)
}
surface SearchIndexRuntimeSurface {
facing _: SearchRuntime
provides:
SearchIndexUpdated(post)
SearchIndexUpdated(media)
}
value StemmerLanguage {
-- Snowball stemmers for 24 languages
-- ISO 639-1 to Snowball mapping
-- Applied to both indexing and query processing
code: String
}
surface StemmerLanguageSurface {
context language: StemmerLanguage
exposes:
language.code
}
entity PostSearchIndex {
-- Full-text index projection
-- Indexed fields: title, excerpt, content, tags, categories
-- Plus all translation titles, excerpts, and content
post: post/Post
stemmed_content: String
}
entity MediaSearchIndex {
-- Full-text index projection
-- Indexed fields: title, alt, caption, original_name, tags
-- Plus all translation titles, alts, and captions
media: media/Media
stemmed_content: String
}
invariant CrossLanguageStemming {
-- Search index uses Snowball stemmer matched to content language
-- A post in German is stemmed with the German stemmer
-- Translations are stemmed with their respective language stemmers
-- Query-time stemming matches the index language
}
rule SearchPosts {
when: SearchPostsRequested(query, filters)
-- Full-text search with optional filters:
-- status, tags, categories, language, missingTranslationLanguage,
-- year, month, date range (from/to)
-- Returns paginated results with total count
let stemmed_query = stem(query, detect_language(query))
let matched = search_fts(PostSearchIndex, stemmed_query, filters)
ensures: SearchResults(
posts: matched,
total: matched.count,
offset: filters.offset,
limit: filters.limit
)
}
rule SearchMedia {
when: SearchMediaRequested(query)
let stemmed_query = stem(query, detect_language(query))
let matched = search_fts(MediaSearchIndex, stemmed_query)
ensures: SearchResults(
media: matched
)
}
rule IndexPost {
when: SearchIndexUpdated(post)
-- Stems: title + excerpt + content + tags + categories
-- Plus all translations' title + excerpt + content
let all_text = concat_post_text(post)
-- Concatenates: post.title, post.excerpt, post.content,
-- join(post.tags, " "), join(post.categories, " "),
-- and all translations' title, excerpt, content
let index_entry = PostSearchIndex{post: post}
ensures:
if exists index_entry:
index_entry.stemmed_content = stem(all_text)
else:
PostSearchIndex.created(post: post, stemmed_content: stem(all_text))
}
rule IndexMedia {
when: SearchIndexUpdated(media)
-- Stems: title + alt + caption + original_name + tags
-- Plus all translations' title, alt, caption
let all_text = concat_media_text(media)
-- Concatenates: media.title, media.alt, media.caption,
-- media.original_name, join(media.tags, " "),
-- and all translations' title, alt, caption
let index_entry = MediaSearchIndex{media: media}
ensures:
if exists index_entry:
index_entry.stemmed_content = stem(all_text)
else:
MediaSearchIndex.created(media: media, stemmed_content: stem(all_text))
}