chore: and more alignment of spec and plan and app
This commit is contained in:
@@ -15,6 +15,17 @@ use "./tabs.allium" as tabs
|
|||||||
surface User {
|
surface User {
|
||||||
provides: ImportAnalyzeRequested(definition_id, file_path)
|
provides: ImportAnalyzeRequested(definition_id, file_path)
|
||||||
provides: ImportExecuteRequested(definition_id)
|
provides: ImportExecuteRequested(definition_id)
|
||||||
|
provides: SiteValidationScanRequested()
|
||||||
|
provides: SiteValidationApplyRequested(report)
|
||||||
|
provides: TranslationValidationScanRequested()
|
||||||
|
provides: TranslationValidationFixRequested(report)
|
||||||
|
provides: DuplicateSearchRequested()
|
||||||
|
provides: DuplicatePairDismissed(post_id_a, post_id_b)
|
||||||
|
provides: DuplicatePairsBatchDismissed(pair_ids)
|
||||||
|
provides: MenuSaveRequested()
|
||||||
|
provides: MenuItemAdded(kind, data)
|
||||||
|
provides: MenuItemDeleted(item_id)
|
||||||
|
provides: MenuItemMoved(item_id, direction)
|
||||||
}
|
}
|
||||||
|
|
||||||
-- ─── Dashboard (no tab active) ───────────────────────────────
|
-- ─── Dashboard (no tab active) ───────────────────────────────
|
||||||
@@ -115,27 +126,47 @@ config {
|
|||||||
|
|
||||||
-- ─── Menu editor actions ────────────────────────────────────
|
-- ─── Menu editor actions ────────────────────────────────────
|
||||||
|
|
||||||
-- Add item (toolbar buttons):
|
rule MenuAddItem {
|
||||||
-- Page entry: opens lazy-loaded page picker (posts with "page" category)
|
when: MenuItemAdded(kind, data)
|
||||||
-- Category archive entry: opens lazy-loaded category picker
|
-- kind = page: opens lazy-loaded page picker (posts with "page" category)
|
||||||
-- Submenu: creates empty container node for nesting children
|
-- kind = category_archive: opens lazy-loaded category picker
|
||||||
-- Home entry: always available, maximum one allowed
|
-- kind = submenu: creates empty container node for nesting children
|
||||||
|
-- kind = home: always available, maximum one allowed
|
||||||
|
ensures: MenuTreeUpdated()
|
||||||
|
}
|
||||||
|
|
||||||
-- Save: serializes tree to OPML 2.0, writes meta/menu.opml
|
rule MenuSave {
|
||||||
|
when: MenuSaveRequested()
|
||||||
|
-- Serializes tree to OPML 2.0, writes meta/menu.opml
|
||||||
|
ensures: MenuFileWritten()
|
||||||
|
}
|
||||||
|
|
||||||
-- Move operations (per-item buttons):
|
rule MenuMoveItem {
|
||||||
|
when: MenuItemMoved(item_id, direction)
|
||||||
|
-- direction = up | down | indent | unindent
|
||||||
-- Up/Down: reorder within same nesting level
|
-- Up/Down: reorder within same nesting level
|
||||||
-- Indent: nest under previous sibling (becomes child)
|
-- Indent: nest under previous sibling (becomes child)
|
||||||
-- Unindent: move to parent's level (becomes next sibling of parent)
|
-- Unindent: move to parent's level (becomes next sibling of parent)
|
||||||
-- Home item: protected — cannot be moved or deleted
|
requires: not is_home_item(item_id)
|
||||||
|
-- Home item is protected: cannot be moved or deleted
|
||||||
|
ensures: MenuTreeUpdated()
|
||||||
|
}
|
||||||
|
|
||||||
|
rule MenuDeleteItem {
|
||||||
|
when: MenuItemDeleted(item_id)
|
||||||
|
requires: not is_home_item(item_id)
|
||||||
|
-- Removes item and all children, no confirmation dialog
|
||||||
|
ensures: MenuTreeUpdated()
|
||||||
|
}
|
||||||
|
|
||||||
-- Drag-and-drop reorder:
|
-- Drag-and-drop reorder:
|
||||||
-- Drag handle on each item
|
-- Drag handle on each item
|
||||||
-- Auto-expand collapsed submenus on hover (450ms delay)
|
-- Auto-expand collapsed submenus on hover (450ms delay)
|
||||||
-- Drop indicators show target position and nesting level
|
-- Drop indicators show target position and nesting level
|
||||||
|
|
||||||
-- Delete: removes item and all children, no confirmation dialog
|
config {
|
||||||
-- Exception: home item cannot be deleted (button hidden)
|
menu_drag_expand_delay: Integer = 450
|
||||||
|
}
|
||||||
|
|
||||||
-- ─── Metadata diff view ──────────────────────────────────────
|
-- ─── Metadata diff view ──────────────────────────────────────
|
||||||
|
|
||||||
@@ -177,35 +208,149 @@ config {
|
|||||||
-- documentation: renders DOCUMENTATION.md as styled HTML
|
-- documentation: renders DOCUMENTATION.md as styled HTML
|
||||||
-- api_documentation: renders API.md as styled HTML
|
-- api_documentation: renders API.md as styled HTML
|
||||||
|
|
||||||
-- ─── Validation views ────────────────────────────────────────
|
-- ─── Site validation view ───────────────────────────────────
|
||||||
|
|
||||||
-- site_validation: checks generated site for broken links, missing assets
|
-- Compares sitemap.xml to generated HTML files on disk.
|
||||||
-- translation_validation: checks translation completeness across languages
|
-- Detects missing pages, orphan HTML, and stale content.
|
||||||
|
|
||||||
-- ─── Validation view actions ────────────────────────────────
|
value SiteValidationReport {
|
||||||
|
expected_url_count: Integer
|
||||||
|
existing_html_count: Integer
|
||||||
|
missing_url_paths: List<String> -- in sitemap, no HTML on disk
|
||||||
|
extra_url_paths: List<String> -- HTML on disk, not in sitemap
|
||||||
|
updated_post_url_paths: List<String> -- source .md newer than HTML
|
||||||
|
}
|
||||||
|
|
||||||
-- Site validation:
|
-- Layout: scan button, summary line, three URL list sections.
|
||||||
-- Scans generated HTML for broken internal links, missing assets
|
-- Summary: "Expected URLs: N — Existing HTML URLs: N — Missing: N — Extra: N — Updated: N"
|
||||||
-- Results: list of issues with file path + line + description
|
-- Each section: heading, list of URL paths, or "None found".
|
||||||
-- No auto-fix actions — informational only
|
|
||||||
|
|
||||||
-- Translation validation:
|
rule SiteValidationScan {
|
||||||
-- Checks translation completeness across configured blog languages
|
when: SiteValidationScanRequested()
|
||||||
-- Results: matrix of posts x languages, missing translations highlighted
|
-- Parses <loc> entries from sitemap.xml into expected URL set
|
||||||
-- No auto-fix actions — informational only
|
-- Scans HTML output dir for index.html files (zero-byte = missing)
|
||||||
|
-- Compares source .md mtime against generated HTML mtime
|
||||||
|
ensures: SiteValidationReport
|
||||||
|
}
|
||||||
|
|
||||||
-- ─── Find duplicates view ────────────────────────────────────
|
rule SiteValidationApply {
|
||||||
|
when: SiteValidationApplyRequested(report)
|
||||||
|
-- Classifies affected paths into generation sections (core, single, category, tag, date)
|
||||||
|
-- Renders only affected sections in parallel
|
||||||
|
-- Deletes extra HTML files, removes empty directories
|
||||||
|
-- Regenerates calendar if anything changed
|
||||||
|
-- Rebuilds search index if anything rendered or deleted
|
||||||
|
-- Toast: "Validation applied: N rendered, N deleted"
|
||||||
|
ensures: GenerateSiteRequested(sections: report.affected_sections)
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── Translation validation view ───────────────────────────
|
||||||
|
|
||||||
|
-- Checks translation integrity across DB rows and filesystem files.
|
||||||
|
-- NOT a matrix view — it is a list of issues found.
|
||||||
|
|
||||||
|
value TranslationValidationReport {
|
||||||
|
checked_database_row_count: Integer
|
||||||
|
checked_filesystem_file_count: Integer
|
||||||
|
invalid_database_rows: List<TranslationValidationIssue>
|
||||||
|
invalid_filesystem_files: List<TranslationValidationIssue>
|
||||||
|
}
|
||||||
|
|
||||||
|
value TranslationValidationIssue {
|
||||||
|
issue: String
|
||||||
|
-- Issue kinds:
|
||||||
|
-- missing_source_post: translationFor points to nonexistent post
|
||||||
|
-- same_language_as_canonical: translation language matches source post language
|
||||||
|
-- do_not_translate_has_translations: source post is doNotTranslate but has translations
|
||||||
|
-- content_in_database: published translation still has content in DB (should be on disk)
|
||||||
|
translation_id: String?
|
||||||
|
translation_for: String
|
||||||
|
canonical_language: String?
|
||||||
|
translation_language: String
|
||||||
|
title: String?
|
||||||
|
file_path: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Layout: Revalidate + Fix Issues buttons, summary line, two issue sections.
|
||||||
|
-- Summary: "Checked DB rows: N — Checked files: N — Invalid DB rows: N — Invalid files: N"
|
||||||
|
-- Each issue rendered as card with coloured left border.
|
||||||
|
-- Card shows: issue label, source post ID, translation ID, title, languages, file path.
|
||||||
|
|
||||||
|
rule TranslationValidationScan {
|
||||||
|
when: TranslationValidationScanRequested()
|
||||||
|
-- Database pass: checks all translation rows for integrity issues
|
||||||
|
-- Filesystem pass: scans posts/ for translation .md files, checks frontmatter
|
||||||
|
ensures: TranslationValidationReport
|
||||||
|
}
|
||||||
|
|
||||||
|
rule TranslationValidationFix {
|
||||||
|
when: TranslationValidationFixRequested(report)
|
||||||
|
-- content_in_database: flushes content to .md file, sets content = null in DB
|
||||||
|
-- missing_source_post | same_language_as_canonical | do_not_translate:
|
||||||
|
-- DB issues: deletes translation row
|
||||||
|
-- Filesystem issues: deletes the .md file
|
||||||
|
-- After fix: automatically re-validates
|
||||||
|
-- Toast: "Deleted N DB rows and N files, flushed N translations to disk"
|
||||||
|
ensures: TranslationValidationScan
|
||||||
|
}
|
||||||
|
|
||||||
|
-- ─── Find duplicates view ──────────────────────────────────
|
||||||
|
|
||||||
-- Uses embedding vectors to find semantically similar posts.
|
-- Uses embedding vectors to find semantically similar posts.
|
||||||
-- Shows candidate pairs with similarity score.
|
-- Gate: requires semanticSimilarityEnabled in project metadata.
|
||||||
-- See embedding.allium for vector details.
|
-- If disabled: shows "Semantic similarity is not enabled" message.
|
||||||
|
|
||||||
-- ─── Find duplicates actions ────────────────────────────────
|
value DuplicateSearchResult {
|
||||||
|
pairs: List<DuplicatePair>
|
||||||
|
}
|
||||||
|
|
||||||
-- Uses HNSW index of post embedding vectors for similarity search
|
value DuplicatePair {
|
||||||
-- Shows candidate pairs: post A title, post B title, score (0.0-1.0)
|
post_id_a: String
|
||||||
-- Click on either post title opens it in a tab (preview intent)
|
title_a: String
|
||||||
-- No auto-merge or auto-delete actions
|
post_id_b: String
|
||||||
|
title_b: String
|
||||||
|
similarity: Decimal -- 0.0 to 1.0
|
||||||
|
exact_match: Boolean -- true if titles + content identical
|
||||||
|
}
|
||||||
|
|
||||||
|
-- Layout: header, count, paginated pair list (500 per page).
|
||||||
|
-- Each pair row: checkbox, post A title (clickable), arrow, post B title (clickable),
|
||||||
|
-- similarity badge ("Exact duplicate" or "N% similar"), Dismiss button.
|
||||||
|
-- Exact matches styled distinctly. Batch controls: Check All, Uncheck All,
|
||||||
|
-- Dismiss Checked (N). Refresh button.
|
||||||
|
|
||||||
|
rule DuplicateSearch {
|
||||||
|
when: DuplicateSearchRequested()
|
||||||
|
requires: semantic_similarity_enabled
|
||||||
|
-- Loads USearch vector index for project
|
||||||
|
-- For each indexed post: search 21 nearest neighbors
|
||||||
|
-- similarity = max(0, 1 - distance)
|
||||||
|
-- Filter: similarity >= threshold, exclude dismissed pairs
|
||||||
|
-- For 100% embedding similarity: load post bodies, compare title+content
|
||||||
|
-- If identical: exact_match = true
|
||||||
|
-- Sort: exact matches first, then descending similarity
|
||||||
|
ensures: DuplicateSearchResult
|
||||||
|
}
|
||||||
|
|
||||||
|
rule DuplicateDismiss {
|
||||||
|
when: DuplicatePairDismissed(post_id_a, post_id_b)
|
||||||
|
-- Inserts into dismissed_duplicate_pairs with canonical ID ordering
|
||||||
|
-- Excluded from future searches
|
||||||
|
-- See schema.allium DismissedDuplicatePair
|
||||||
|
ensures: PairDismissed(post_id_a, post_id_b)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule DuplicateBatchDismiss {
|
||||||
|
when: DuplicatePairsBatchDismissed(pair_ids)
|
||||||
|
-- Batch insert in chunks of 100
|
||||||
|
ensures: for pair in pair_ids: PairDismissed(pair)
|
||||||
|
}
|
||||||
|
|
||||||
|
config {
|
||||||
|
duplicate_similarity_threshold: Decimal = 0.92
|
||||||
|
duplicate_page_size: Integer = 500
|
||||||
|
duplicate_neighbor_count: Integer = 21
|
||||||
|
}
|
||||||
|
|
||||||
-- ─── Import analysis view ───────────────────────────────────
|
-- ─── Import analysis view ───────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -145,3 +145,42 @@ invariant ArchiveDayBlocks {
|
|||||||
-- Archive/list pages group posts by day
|
-- Archive/list pages group posts by day
|
||||||
-- Each day block has a date header and the posts for that day
|
-- Each day block has a date header and the posts for that day
|
||||||
}
|
}
|
||||||
|
|
||||||
|
-- ============================================================================
|
||||||
|
-- SEARCH INDEX: PAGEFIND
|
||||||
|
-- ============================================================================
|
||||||
|
|
||||||
|
-- Pagefind builds a client-side full-text search index from generated HTML.
|
||||||
|
-- Uses the `pagefind` crate (library API), not a CLI subprocess.
|
||||||
|
-- Runs as the final step of the generation pipeline, after all HTML is written.
|
||||||
|
|
||||||
|
rule BuildSearchIndex {
|
||||||
|
when: GenerateSiteCompleted(generation)
|
||||||
|
-- Only runs if any pages were rendered or deleted in this generation pass.
|
||||||
|
-- Separate index per language:
|
||||||
|
-- Main language: source = {html}/, output = {html}/pagefind/
|
||||||
|
-- Each additional language: source = {html}/{lang}/, output = {html}/{lang}/pagefind/
|
||||||
|
-- Each index built with force_language set to that language code.
|
||||||
|
for lang in {generation.language} + (generation.blog_languages - {generation.language}):
|
||||||
|
ensures: PagefindIndexBuilt(lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
invariant PagefindHtmlMarking {
|
||||||
|
-- Single-post templates must include data-pagefind-body attribute
|
||||||
|
-- on the <article> element to scope indexing to post content only.
|
||||||
|
-- Pagefind ignores elements without this attribute.
|
||||||
|
}
|
||||||
|
|
||||||
|
invariant PagefindAssets {
|
||||||
|
-- Generated output includes Pagefind UI assets per language:
|
||||||
|
-- {prefix}/pagefind/pagefind-ui.css
|
||||||
|
-- {prefix}/pagefind/pagefind-ui.js
|
||||||
|
-- where prefix is "" for main language, "{lang}" for additional languages.
|
||||||
|
-- Frontend templates reference these via language_prefix variable.
|
||||||
|
-- Assets are bundled locally — no external CDN references.
|
||||||
|
}
|
||||||
|
|
||||||
|
config {
|
||||||
|
pagefind_threshold: Integer = 0
|
||||||
|
-- Minimum pages to trigger indexing (0 = always if any rendered/deleted)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user