49 KiB
49 KiB
Spec Gaps — Allium Specs vs Code vs Tests
Gap categories: SC = spec correct, fix code | CS = code correct, update spec | ST = write test | SD = decide | SI = fix internal spec inconsistency
A. Spec Claims Not Fulfilled by Code
A1. Code Must Change (spec is normative)
| ID | Gap | Spec | Code | Path |
|---|---|---|---|---|
| A1-1 | archived→draft or archived→published transition |
post.allium:121-122 | unarchive_post/1 implemented, publish_post already handled archived→published |
Resolved: unarchive_post/1 in posts.ex restores content from disk, UI wired via quick actions, 4 tests added |
| A1-2 | DeletePost must delete translations + translation files |
post.allium:209-212 | delete_post/1 now fetches translations before cascade-delete and removes their files from disk |
Resolved: translation file cleanup added to delete_post/1 in posts.ex, test added |
| A1-3 | engine_side_effects.allium:73-74 | publish_post now deletes old file when file_path changes |
Resolved: old file deletion added to publish_post/1 in posts.ex, test added |
|
| A1-4 | doNotTranslate: false written to frontmatter despite "only when true" |
frontmatter.allium:398 | file_sync.ex:78 now converts false→nil so serializer omits the key |
Resolved: doNotTranslate omitted from frontmatter when false, test added |
| A1-5 | editor_post.allium:183-188 | PostEditor schedules auto-save via parent timer on dirty change | Resolved: 3000ms idle auto-save timer in Bridges, tab-switch save in ShellLive, cancel on manual save, 3 tests added | |
| A1-6 | preview.allium:53-93 | Preview.Router matches post/archive/home/language routes and renders on-demand via Rendering |
Resolved: Preview.Router implements on-demand template rendering for post, archive, home, date, tag, category, page, and language-prefixed routes; static file fallback retained for non-HTML assets (pagefind, feeds); 6 tests added |
|
| A1-7 | template_context.allium:267-277 | resolve_post_template_slug/3 implements tag→category cascade; all callers (preview, generation) updated |
Resolved: resolve_post_template_slug/3 in template_selection.ex, callers in preview.ex, router.ex, outputs.ex updated, 8 tests added |
|
| A1-8 | ValidateLiquid/ValidateScript before publish |
template.allium:110, script.allium:165 | publish_template validates Liquid via Liquex.parse, publish_script validates Lua via BDS.Scripting.validate |
Resolved: validation gates added to publish_template/1 and publish_script/1, invalid content returns `{:error, {:invalid_liquid |
| A1-9 | editor_tags.allium | ColourPicker hook + popover with 17 preset swatches grid and custom hex input, wired to both create and edit forms |
Resolved: replaced native <input type="color"> with ColourPickerPopover component (17 presets, custom hex #RRGGBB, immediate selection), JS hook for click-away dismiss, 1 test added |
|
| A1-10 | engine_side_effects.allium:151-153 | create_template now computes file_path and writes template file with YAML frontmatter on create |
Resolved: create_template/1 writes templates/{slug}.liquid on create, next_template_file_path always computes path, 1 test added |
|
| A1-11 | preview.allium:47-48 | stop_preview now closes the listener, parks the reply, and drains monitored inflight request tasks before reporting stopped |
Resolved: acceptor transfers socket ownership to each request task; GenServer monitors inflight tasks, begin_graceful_stop stops accepting and finalizes via :DOWN/:drain_timeout (5s force-kill cap), 1 test added |
|
| A1-12 | generation.allium:208 | Functional client-side search: PagefindUI defined in bundled pagefind-ui.js, fragment index records url/title/body-scoped text per page, search-runtime wires it up |
Resolved: bundled real PagefindUI (fetch index, ranked full-text match, highlighted excerpts) + pagefind-ui.css as local assets read into Pagefind; index scoped to data-pagefind-body (unmarked pages excluded per PagefindHtmlMarking), title from <title>/<h1>; localized "No results found" label via data-search-no-results (de/fr/it/es); 3 unit tests added |
|
| A1-13 | sidebar_views.allium:651-770 | git_view/1 now builds a full layout: "git" view from BDS.Git (repository/remote_state/status/history); SidebarComponents renders active + not_a_repo states |
Resolved: git_view/1 in sidebar.ex assembles branch/upstream/ahead/behind, status files, paginated history (20/page); render_git_sidebar renders branch header, sync legend, fetch/pull/push/prune-lfs buttons, commit form, clickable status files (open git_diff), history entries; shell_live wires git_commit (closes git_diff tabs), git_fetch/git_pull/git_push/git_prune_lfs, git_initialize; BDS.Git.history enriched with author/date, BDS.Git.set_remote/2 added; i18n for de/fr/it/es; 3 shell tests + git author/date assertions added |
|
| A1-14 | embedding.allium:44-53, invariants RealNeuralModel/ModelCaching/VectorCacheInDb | Backends.Neural runs intfloat/multilingual-e5-small (e5 weights behind the Xenova id) via Bumblebee+EXLA |
Resolved (core): added bumblebee/nx/exla deps; Backends.Neural is a lazily-loaded GenServer that builds the Bumblebee text-embedding serving on first request ("query: " prefix + mean pooling + L2 norm), downloads+caches the model under the app data dir (ModelCaching), and is wired into the supervision tree when configured; vectors now persisted as packed little-endian Float32 BLOB (384×4=1536 bytes) instead of JSON text (VectorCacheInDb) with migration recreating embedding_keys.vector as BLOB; InApp demoted to documented offline/test stub; test config uses the stub so the suite stays offline; spec EmbeddingModel clarified (Xenova id ↔ intfloat weights via Bumblebee); batched inference via optional embed_many/2 backend callback (configurable batch_size/sequence_length; rebuild/index/repair embed in chunks instead of one post at a time) + NativeAcceleratedExecution invariant added to spec; 4 tests added (BLOB round-trip, batched-rebuild, Neural model_info/behaviour). Deferred: A1-14b USearch HNSW index, A1-14c Apple GPU (EMLX). |
|
| A1-14b | embedding.allium config/FindSimilar/DebouncedPersistence | Embeddings.Index is now an HNSW (hnswlib) ANN index with debounced persistence |
Resolved: rewrote Embeddings.Index as a DB-free GenServer wrapping an hnswlib HNSW graph (cosine, M=16, efConstruction=128, efSearch=64) — O(n·log n) build, O(log n) queries, replacing the O(n²) JSON cosine snapshot; per-project in-memory index + label→post_id map; 5s debounced save_index + .meta.json sidecar, force-save on project switch (set_active_project) and shutdown (terminate), forget/1 on project delete; lazy reload from disk with rebuild-from-DB self-heal on miss; find_similar/find_duplicates/compute_similarities rewired (no brute-force fallback); USearch has no Elixir binding so hnswlib provides the identical HNSW algorithm/params (spec reconciled); supervision + dialyzer PLT updated; tests updated for debounced/binary persistence + self-heal. Follow-up hardening: explicit rebuild now forces re-embedding regardless of content_hash (ReindexAll), and model-unavailable errors propagate cleanly (post saves degrade to unindexed + log; rebuild/index return {:error, reason} surfaced as a failed task with a user-facing message instead of crashing). |
|
| A1-14c | embedding.allium invariant NativeAcceleratedExecution | Backends.Neural now selects the defn compiler at serving-build time: Apple GPU via EMLX (MLX/Metal) on arm64 macOS, EXLA-CPU elsewhere |
Resolved: added {:emlx, "~> 0.2.0"} dep (ships precompiled MLX binaries; EMLX 0.2.0 implements both EMLX.Backend and the Nx.Defn.Compiler behaviour, GPU-default); Backends.Neural gained a pure select_accelerator/3 policy (:auto prefers EMLX only when available and on Apple Silicon; explicit :emlx/:exla honoured; forced :emlx degrades to EXLA when unavailable so misconfigured hosts still run), current_accelerator/0, and defn_options/1; build_serving places params on {EMLX.Backend, device: :gpu} and compiles with EMLX for the EMLX path, keeps EXLA otherwise; new accelerator: :auto config key; spec NativeAcceleratedExecution + EmbeddingModel updated; PLT app added; 7 tests added (offline — test config still uses the InApp stub). |
|
| A1-15 | preview.allium (no invariant), generation.allium (no invariant) | Generation uses only published .md file content (Generation.Data snapshots set content: nil); preview includes published+draft posts and prefers DB content over file (Preview.Router queries :published/:draft, uses editor_body) |
Resolved: added PreviewDraftOverlay invariant to preview.allium and GenerationPublishedOnly invariant to generation.allium; both cross-reference each other; code already correct, 3 tests added for draft-in-preview behavior |
|
| A1-16 | project.allium PublicContentLivesInProjectFolder / PrivateArtifactsLiveInOsAppDir / DataPathNotPersistedInProjectJson / DiscoverProjectDataPath |
Public content now lives under a per-user default content location, never the repo | Resolved: project_data_dir/1 drops the priv/data/projects/<id> repo fallback — a project without an explicit data_path resolves to default_content_root()/<id> (configurable via :default_content_root, else ~/bds), never the repo or private_dir; the default project is now created on first launch with an explicit data_path under that location and its folder is mkdir'd (PublicContentLivesInProjectFolder); added Projects.private_dir/0, default_content_root/0, and a machine-local project registry (registry_path/0 → project_registry.json under private_dir, written on create/ensure-default, removed on delete) that remembers each project's folder without embedding it in meta/project.json (DataPathNotPersistedInProjectJson/DiscoverProjectDataPath — already satisfied since project.json never serializes data_path); delete_project removes app-managed folders (those under default_content_root) but preserves user-chosen external folders; committed priv/data/projects/default/ content removed from the repo and /priv/data/projects/ git-ignored; test config redirects :default_content_root to a temp dir; 4 tests added (default folder outside repo/private, no-repo fallback, registry round-trip, registry cleanup on delete). |
|
| A1-17 | bds2://new-post blogmark deep link is never received or routed |
script.allium:74 (BlogmarkReceived), script.allium:233-268 (ExecuteTransform/TransformTrigger), editor_settings.allium:141-143 (BookmarkletCopy) |
BDS.Desktop.DeepLink subscribes to OS {:open_url, ...} events and routes bds2:// links to the shell; BDS.Blogmark parses + runs transforms + creates the draft; ShellLive opens it and surfaces toasts |
Resolved: added BDS.Blogmark.parse_deep_link/1 (parses bds2://new-post?title=&url=&content=&tags=&categories= into a {title, content?, tags, categories, url} candidate, rejecting unsupported scheme/action) and receive_deep_link/3 (runs BDS.Scripts.Transforms.run/3 then creates a draft post, defaulting categories from blogmark_category only when neither the link nor a transform set one); BDS.Desktop.DeepLink GenServer subscribes to Desktop.Env {:open_url, [url]} events and broadcasts {:blogmark_deep_link, url} over PubSub (added to the desktop supervision tree, guarded for headless/test); ShellLive.handle_info({:blogmark_deep_link, url}) runs the import against the active project, opens the new post tab (route "post"), and appends transform toasts/errors to the output panel (warns when no project is open). OS-level scheme registration (macOS CFBundleURLTypes in the packaged Info.plist) is documented in BDS.Desktop.DeepLink — no app-bundle pipeline exists in the repo yet. i18n added for Blogmark/"Open a project before importing a blogmark." (de/fr/it/es); 13 tests added (5 parse, 5 receive incl. transforms + default category, 3 deep-link routing, 1 shell create+open). |
A2. Spec Should Update (code is normative)
| ID | Gap | Spec | Code | Path |
|---|---|---|---|---|
| A2-1 | editor_post.allium:159-164 | Only markdown+preview; visual normalizes to markdown | Resolved: spec updated to 2 modes (markdown/preview), visual/WYSIWYG dropped | |
| A2-2 | template.allium, script.allium | Both have project_id, per-project uniqueness |
Resolved: spec updated — added project_id to entities, scoped uniqueness invariants and create rules per project |
|
| A2-3 | {tags: [...]} wrapper |
frontmatter.allium:255-273 | Code writes bare array [...] |
Resolved: spec updated — removed wrapper object, TagEntry is now the top-level value, bare array in invariant, camelCase keys |
| A2-4 | frontmatter.allium:174 | Code wraps with --- delimiters |
Resolved: spec updated — format comment now says gray-matter style with --- delimiters | |
| A2-5 | frontmatter.allium:107-117 | Code writes status, createdAt, updatedAt, publishedAt | Resolved: spec updated — TranslationFrontmatter now includes status, created_at, updated_at, published_at; TranslationFilesInheritCanonicalMetadata renamed to TranslationFrontmatterRoundtrip; translation.allium invariant updated to TranslationFilesCarryFullMetadata | |
| A2-6 | stemmed_content |
search.allium:40-54 | FTS5 per-field stemmed columns | Resolved: spec updated — PostSearchIndex has title/excerpt/content/tags/categories; MediaSearchIndex has title/alt/caption/original_name/tags; SearchMedia now accepts filters; index rules use delete-and-reinsert with per-field stemming |
| A2-7 | generation.allium:142-147 | Code paginates | Resolved: spec updated — GenerateTagPages now paginated like categories, using max_posts_per_page | |
| A2-8 | generation.allium:151-159 | Code also generates day-level | Resolved: spec updated — GenerateDateArchivePages now includes day-level archives, all three levels paginated | |
| A2-9 | menu.allium:20-26 | Purely file-based OPML, no DB table | Resolved: spec updated — entity Menu changed to value Menu, file-only model with OPML persistence, added LoadMenu/SyncMenuFromFilesystem rules |
|
| A2-10 | layout.allium:235-240 | [:tasks, :output, :post_links, :git_log] |
Resolved: spec already lists tasks/output/post_links/git_log with availability and fallback rules matching code | |
| A2-11 | sidebar_views.allium | Only "Working tree" item | Moved to A1-13: backend code exists in BDS.Git, sidebar must wire it up | |
| A2-12 | post.allium:21 | Unbounded numeric suffix | Resolved: spec updated — uniqueness comment now says unbounded numeric suffix, no 999 cap or timestamp fallback | |
| A2-13 | engine_side_effects.allium:117 | Synchronous | Resolved: spec updated — import thumbnail generation now says synchronous (awaited, logged on error), matching code; summary table changed from async to sync |
|
| A2-14 | schema.allium:291 | Code has :file, :tool instead of :video |
Resolved: spec updated — modality enum now lists "text" | "image" | "audio" | "file" | "tool", matching code | |
| A2-15 | frontmatter.allium values | Code uses camelCase for all metadata JSON | Resolved: all value types in frontmatter.allium updated to camelCase field names; added CamelCaseKeys invariant; surfaces updated; also added linkedPostIds to MediaSidecar (C-2) and projectId to TemplateFrontmatter/ScriptFrontmatter (B1-9) | |
| A2-16 | search.allium:26-31 | Library determines which have algorithms vs passthrough | Resolved: spec updated — StemmerLanguage comment now says "Snowball stemmers via library (Stemex); languages with algorithm get real stemming, others pass through" | |
| A2-17 | provider_package_ref on AiModel |
schema.allium:282 | Not in code; legacy field not needed | Resolved: dropped from AiModel entity and AiModelRecordSurface in schema.allium; DB column retained (migration artifact) |
B. Code Behavior Not in Spec
B1. Must Add to Spec (domain-level, affects behavior)
| ID | Behavior | Code Location | Path |
|---|---|---|---|
tool_surfaces.ex |
Resolved: added InlineSurface value type with all 9 discriminators, supporting value types (SurfaceAction, ChartSeries, MindmapNode, FormField, FieldOption, TabPanel), inline_surfaces on ChatMessage, InlineSurfaceRendering guarantee, and surface_form_debounce_ms config to editor_chat.allium; updated StructuredRenderTools invariant in ai.allium to list all 9 types | ||
lib/bds/posts/auto_translation.ex |
Resolved: distilled into translation.allium — added AutoTranslationControlSurface (PostSavedForAutoTranslation reactive + FillMissingTranslationsRequested batch triggers), three rules (ScheduleAutoTranslation draft-per-missing-language + media cascade, AutoTranslatePost upsert/auto-publish primitive, AutoTranslateMediaCascade linked-media per-language tasks, FillMissingTranslations published-only batch emitting ProgressReported + FillMissingTranslationsCompleted), three invariants (AutoTranslationGatedByEndpoint, AutoTranslationSkipsDoNotTranslate, AutoTranslationOnlyMissingLanguages), and auto_translation_task_group_name config; allium check passes |
||
lib/bds/desktop/shell_live/settings_editor/ |
Resolved: distilled into editor_settings.allium — added SettingsTechnologySection (semantic_similarity toggle + read-only scripting-runtime note, saved with project metadata), SettingsDataSection (rebuild_targets), and technology_section/data_section on SettingsView; reconciled SettingsMCPSection/MCPAgentRow to code (dropped non-existent status badge; added is_supported/config_path; only Claude Code + GitHub Copilot supported); updated TechnologySection/MCPSection/DataMaintenanceSection guarantees (7 rebuild buttons incl. Rebuild Embedding Index) and SettingsRebuild rule entity_type (+embedding); allium check passes |
||
:style), not settings section |
lib/bds/desktop/shell_live/settings_editor/style_editor.ex |
Resolved: editor_settings.allium Style view section now frames it as its own style singleton tab (cross-ref tabs.allium), explicitly NOT a SettingsView collapsible section; added SeparateTab guarantee (requires active project), documented 20 named Pico themes and the theme display-name transform ("-"→" ", capitalise); allium check passes |
|
published_* snapshot fields on Post for diffing |
lib/bds/posts/post.ex:61-65 |
Resolved: added published_title/content/tags/categories/excerpt snapshot fields to the Post entity in post.allium, noting their role in changes_affect_published_content / ReopenPublishedPost (schema.allium already listed them) | |
lib/bds/rendering/ |
Resolved: new rendering.allium distills the shared render subsystem — 3 custom filters (i18n/markdown/slugify), markdown→HTML + URL rewriting, built-in [[...]] macros (youtube/vimeo/gallery/photo_archive/tag_cloud) with isolation invariant, links/languages (LinkContext, language prefix, backlinks), RenderLabels (content-language gettext + months), and post/list/not-found assign builders; added SharedRenderPathForPreviewAndGeneration invariant and wired into bds.allium |
||
lib/bds/generation/outputs.ex:344-345 |
Resolved: added FileGenerated("404.html") (+ {lang}/404.html per blog language) to GenerateCoreSectionPages in generation.allium |
||
| B1-8 | linkedPostIds in media sidecar |
lib/bds/media/sidecars.ex:42 |
Resolved: added to MediaSidecar value in frontmatter.allium (with A2-15) |
| B1-9 | projectId in template/script frontmatter |
templates.ex:337, scripts.ex:268 |
Resolved: added projectId to TemplateFrontmatter and ScriptFrontmatter in frontmatter.allium (with A2-15) |
media_editor.html.heex:275-303 |
Resolved: editor_media.allium corrected — MediaTranslationEdit opens an "Edit Translation" modal (not inline); added TranslationEditModal guarantee (language hidden field + Title/Alt/Caption inputs, Cancel/Save footer) |
||
lib/bds/desktop/menu_editor/tree_ops.ex |
Resolved: already covered in editor_misc.allium — MenuMoveItem rule (up/down/indent/unindent), DragDrop + MoveDirections guarantees, HomeItemProtection (D1-18); verified against tree_ops.ex | ||
:language_picker overlay with flag emojis |
shell_overlay.html.heex:116-139 |
Resolved: already in modals.allium — LanguagePickerModal/LanguageTarget (flag emoji, name, existing-translation status badge); verified matches overlay code | |
:confirm_dialog generic confirmation |
shell_overlay.html.heex:171-187 |
Resolved: already in modals.allium — ConfirmDialog (title/message, Cancel/Confirm); verified matches overlay code | |
script_editor.html.heex:10-12, template_editor.html.heex:10-12 |
Resolved: added can_publish field, Publish button in HeaderLayout (shown only when draft), ScriptPublishRequested/TemplatePublishRequested surface events and ScriptPublish/TemplatePublish rules (validate-then-publish) to editor_script.allium + editor_template.allium |
||
:import as full editor tab |
lib/bds/ui/import_editor.ex |
Resolved: already in tabs.allium (import singleton/pinned tab + editor route) and fully detailed as ImportAnalysisView in editor_misc.allium | |
:documentation/:api_documentation tab types |
lib/bds/desktop/misc_editor/ |
Resolved: already in tabs.allium (both listed as singleton tabs + editor views: DOCUMENTATION.md / API.md) with DocumentationSurface in editor_misc.allium | |
lib/bds/maintenance/repair.ex |
Resolved: metadata_diff.allium DiffReport entity_type list expanded to all 7 types; RunMetadataDiff notes media/media_translation/script/template/embedding (content_hash) coverage; added RepairMetadataDiffItem rule + surface event documenting the file_to_db/db_to_file dispatch per entity type |
||
lib/bds/tasks.ex:365-386 |
Resolved: added finished_task_ttl (1h) + recent_finished_limit (10) config, FinishedTaskRetention invariant, EvictFinishedTasks rule and FinishedTaskEvictionDue runtime event to task.allium |
||
discard_post_changes/1 |
lib/bds/posts.ex:201-227 |
Resolved: added DiscardPostChangesRequested surface event + DiscardPostChanges rule (requires file_path, restores published file, content=null/status=published, re-syncs links+FTS) to post.allium |
|
replace_media_file/2 with checksum/backup |
lib/bds/media.ex:288-337 |
Resolved: added ReplaceMediaFileRequested surface event + ReplaceMediaFile rule (md5 no-op skip, .bak backup/restore, updates checksum/size/dimensions, synchronous thumbnail regen, FTS re-sync) to media.allium |
B2. Lower Priority (implementation detail or minor)
| ID | Behavior | Code Location | Resolution |
|---|---|---|---|
editor_body/1 content resolver |
lib/bds/posts.ex:234-256 |
Resolved: added editor_body derived field to the Post entity in post.allium (prefer DB draft content, else read markdown body from file, else empty; same for translations) |
|
sync_post_from_file/1 single-post reimport |
lib/bds/posts.ex:259 |
Resolved: added SyncPostFromFileRequested surface event + SyncPostFromFile rule (re-read own .md file, upsert DB, re-sync links) to post.allium |
|
import_orphan_post_file/1 |
lib/bds/posts.ex:293 |
Resolved: added ImportOrphanPostFileRequested surface event + ImportOrphanPostFile rule (import a disk .md with no DB row, reject non-markdown) to post.allium |
|
dashboard_stats/1, post_counts_by_year_month/1 |
lib/bds/posts.ex:416-450 |
Resolved: added ComputeDashboardData rule to editor_misc.allium (status-grouped counts, (year,month) timeline newest-first limited to recent months, plus clouds/recent) |
|
regenerate_missing_thumbnails/2 |
lib/bds/media/thumbnails.ex:51 |
Resolved: added RegenerateMissingThumbnailsRequested surface event + RegenerateMissingThumbnails rule (raster images excl. SVG, regenerate only missing files, background task w/ counts) to media_processing.allium |
|
lib/bds/projects.ex:142-147 |
Resolved: added cache_dir derived field (private_dir/projects/{id}, :project_cache_root override) to the Project entity in project.allium |
||
remove_stale_published_templates |
lib/bds/templates.ex:561 |
Resolved: extended RebuildTemplatesFromFiles in template.allium to prune published templates whose file is neither scanned nor on disk (clearing references first) | |
lib/bds/rendering/labels.ex |
Resolved: captured as RenderLabels value + LabelsUseContentLanguage invariant in rendering.allium (with B1-6) — content-language gettext strings + month names |
||
lib/bds/generation/progress.ex |
Resolved: added GenerationProgressReported runtime event + ProgressReporting guarantee to generation.allium (count-based + phased fractions, via task progress channel) |
C. Internal Spec Inconsistencies
All reconciled to follow code. Specs must be self-consistent and match code.
| ID | Conflict | Resolution | Path | |
|---|---|---|---|---|
| C-1 | cache_read_tokens/cache_write_tokens |
Code has cache tokens → align schema.allium with ai.allium | Resolved: added cache_read_tokens + cache_write_tokens to ChatMessage entity and ChatMessageRecordSurface in schema.allium | |
| C-2 | linkedPostIds; frontmatter.allium MediaSidecar does NOT list it |
Code writes linkedPostIds → add to frontmatter.allium |
Resolved: linkedPostIds added to MediaSidecar in frontmatter.allium (with A2-15) | |
| C-3 | Code writes status/timestamps → update both specs to match code | Resolved: both specs updated (see A2-5) |
D. Spec Claims Not Covered by Tests
D1. No Test Coverage (HIGH priority — invariants/guarantees)
| ID | Claim | Spec | Path |
|---|---|---|---|
| D1-1 | media.allium:108 | Resolved: test added (re-upsert updates not duplicates; direct duplicate insert rejected). Test exposed a real bug — Media.Translation declared the migration's index name but ecto_sqlite3 derives the violated-constraint name from columns, so violations crashed instead of returning a changeset error; fixed unique_constraint name to :media_translations_translation_for_language_index |
|
| D1-2 | translation.allium:94 | Resolved: test added (re-upsert updates not duplicates; direct duplicate insert rejected). Same bug as D1-1 — Posts.Translation declared the migration's index name but ecto_sqlite3 derives the violated-constraint name from columns, so duplicates crashed instead of returning a changeset error; fixed unique_constraint name to :post_translations_translation_for_language_index |
|
| D1-3 | template.allium:65 | Resolved: added 4 tests in template_lookup_priority_test.exs — with no Template rows for the project, load_template_source/3 resolves bundled single-post/post-list/not-found defaults (and still resolves when the project has no templates/ directory at all) |
|
| D1-4 | template.allium:75 | Resolved: added 2 tests in template_lookup_priority_test.exs — a published project Template row with the bundled default slug (single-post) wins over the bundled default both when resolving :post with no explicit slug and when the slug is requested explicitly |
|
| D1-5 | template.allium:179 | Resolved: added BDS.Rendering.LiquidParser, a restricted Liquex parser recognizing only the subset (if/for/assign/render + {{ }} output); any other tag (unless, case, capture, tablerow, cycle, increment, …) leaves unmatched input and fails eos/0. Wired into validate_liquid (publish gate), template_selection.render_template, filters.render_macro_source, and MCP validate_template so validation and rendering share the same surface; 6 parametrized tests added asserting unsupported tags are rejected at publish |
|
| D1-6 | template.allium:191 | Resolved: added LiquidParser.validate/1, which parses with the restricted tag grammar then walks the AST to reject any filter outside the allowed set — 4 standard (escape, url_encode, default, append) + 3 custom (i18n, markdown, slugify). Wired into validate_liquid (publish gate) and MCP validate_template so unsupported filters are rejected even though Liquex would otherwise apply them as built-in standard filters. Spec corrected to 3 custom filters (bundled templates use slugify); 9 tests added (6 unsupported filters rejected, 3 supported filters accepted). |
|
| D1-7 | template.allium:210 | Resolved: LiquidParser.validate/1 now walks the parsed AST for {:op, _} nodes and rejects any comparison operator outside the allowed ==/> subset (!=, <, >=, <=, contains), sharing the publish gate and MCP validate_template surface with the tag/filter checks; spec LiquidOperatorSubset annotated with enforcement note; 10 tests added (5 unsupported operators rejected at publish, 5 supported ==/>/and/or/bare-truthy expressions accepted). |
|
| D1-8 | script.allium:94-95 | Resolved: added test in api_test.exs — an infinite-loop render() macro run with max_reductions: :none (forces the luerl sandbox onto its wall-clock path) and a 150ms timeout returns {:error, :timeout} and terminates within budget (<2s), proving the macro is killed near its budget rather than the default multi-minute script timeout |
|
| D1-9 | script.allium:229-263 | Resolved: the ExecuteTransform rule had no engine — added BDS.Scripts.Transforms.run/3 (+ Scripts.list_transform_scripts/1 ordered by updated_at→slug→id and Scripts.resolved_content/1). The pipeline runs enabled project transforms sequentially on the blogmark candidate with a {source="blogmark", url} context, captures per-script errors without rolling back the last valid candidate (TransformPipelineContinuation), and enforces the toast budget (transform_max_toasts_per_script/transform_max_toasts_total/transform_max_toast_length, new config keys). 6 tests added (ordering, project/disabled scoping, continuation, context, per-script + total toast caps with truncation). Deep-link OS routing into this engine remains future work. |
|
| D1-10 | script.allium:247-249 | Resolved: added focused test in transforms_test.exs — a failing first transform (no prior valid state) does not halt the pipeline: the original input survives, a later enabled transform still runs against it, and every failure is captured per-script in pipeline order tagged with its slug |
|
| D1-11 | ai.allium:375-379 | Resolved: test added in ai_test.exs — a catalog model with a 2,000-token context window plus 40 large seeded turns forces truncation; the captured chat request keeps the system prompt as the first message, drops the oldest pairs first (surviving markers form a contiguous newest suffix, oldest absent), and always retains the newest user turn |
|
| D1-12 | ai.allium:381-385 | Resolved: the round cap is now read from config.chat_max_tool_rounds (config :bds, :chat, max_tool_rounds: 10) via chat_max_tool_rounds/0 in chat.ex instead of a hardcoded attribute, matching the spec wording; test added in ai_test.exs — a LoopingToolRuntime that always returns another tool call (never a final answer) with max_tool_rounds: 3 ends with {:error, %{kind: :tool_loop_exhausted}} after exactly 3 runtime calls (the rounds_left == 0 round short-circuits before contacting the runtime) |
|
| D1-13 | engine_side_effects.allium:99-104 | Resolved: test added in posts_test.exs — a published post is dirtied with unsaved title/content edits (re-indexing the dirty text in FTS), then discard_post_changes/1 restores the published file version (status=published, content=nil, original title) and re-syncs the FTS index so the published terms are searchable again and the discarded edits are gone |
|
| D1-14 | engine_side_effects.allium:128-134 | Resolved: 3 tests added in media_test.exs — replace_media_file/2 copies the new image over the existing path, updates the row (checksum/size/width/height), and regenerates all thumbnails synchronously (present immediately after the call, no .bak backup left); identical-checksum replace is a no-op ({:ok, nil}); unknown media id returns {:error, :not_found} |
|
| D1-15 | action_patterns.allium:84-103 | Resolved: the chain had no handler — added BDS.Desktop.ShellLive.EditorImageDrop (import_and_link/3 runs steps 1-4: import media + synchronous thumbnails + link to post + return  markdown; enrich/3 runs background steps 5-6: AI analysis auto-applied with no modal + auto-translate cascade when do_not_translate == false). PostEditor.handle_event("editor_image_dropped", ...) runs the synchronous chain (works offline since import isn't AI), pushes the cursor insert, and spawns enrich only when airplane mode is off. MonacoEditor JS hook captures image drops on the editor surface and pushes the file path (phx-target={@myself} routes the hook event to the component); i18n for de/fr/it/es. 3 tests added (module chain incl. thumbnails+link+markdown, non-image link form, full LiveView drop in airplane mode asserting import/link/insert with no AI metadata). |
|
| D1-16 | embedding.allium:213-217 | Resolved: 3 tests added in embeddings_test.exs (DebouncedPersistence describe): Index.put/3 schedules a per-project save timer with ~5s remaining (>4000ms, <=5000ms) instead of writing immediately (no embeddings.usearch on disk yet); rapid puts coalesce (each reschedules the single timer, the previous timer is cancelled so Process.read_timer returns false, and still no file after two writes); when the {:save, project_id} debounce message fires the index is persisted to disk and the pending timer cleared. The coalescing test exposed a real bug: handle_call({:put, ...}) replaced the stored entry with build_entry/2's fresh timer: nil entry before schedule_save/2 ran, orphaning the previous debounce timer (left to fire a redundant save) instead of cancelling it; fixed via cancel_pending_save/2 so bulk puts collapse to one deferred write |
|
| D1-17 | editor_settings.allium:81-84 | Resolved: 5 tests added in managed_categories_test.exs covering protected_category?/1 classification and remove_category/4 rejection for all 4 protected categories (article, aside, page, picture) plus non-protected deletion allowed | |
| D1-18 | editor_misc.allium:206-209 | Resolved: 13 tests added for TreePredicates (can_move_up?/can_move_down?/can_indent?/can_unindent? return false for Home, can_delete? false for Home, true for non-Home) + TreeOps (move_selected/indent_selected/unindent_selected/delete_selected no-op for Home, drop_selected no-op when drag item is Home, non-Home operations still work); code guards added to all predicate and operation functions |
D2. No Test Coverage (MEDIUM priority — rules/behaviors)
| ID | Claim | Spec | Path |
|---|---|---|---|
| Resolved: 2 tests added in metadata_test.exs — remove_category removes category+settings from state/files/DB (6 assertions); remove_category is a no-op for non-existent category | |||
| template.allium:105 | Resolved: added create_and_publish_template/1 (validates Liquid, creates published template + writs file), 3 tests added (happy path file assertions, invalid liquid rejection, slug dedup on title conflict) |
||
Resolved: create_and_publish_script/1 implemented in scripts.ex, 4 tests added (happy path with file assertions + macro entrypoint default + invalid Lua rejection + slug dedup on title conflict) |
|||
Resolved: test added asserting two scripts with same title produce unique slugs (dup-slug → dup-slug-2) |
|||
| D2-5 | post.allium:223 | Resolved: test added — creates post with all fields, publishes, parses file back via Frontmatter.parse_document, asserts every field (id, title, slug, excerpt, status, author, language, doNotTranslate, templateSlug, tags, categories, createdAt, updatedAt, publishedAt, body) matches the published DB record |
|
Resolved: 2 tests added — full roundtrip (write sidecar, parse via Sidecar.parse_document/1, assert all fields match DB) + nil conditional fields absent from parsed sidecar |
|||
| frontmatter.allium:398 | Resolved: test added — post with nil excerpt/author/language → those fields absent from published file, 3 refute assertions | ||
| frontmatter.allium:417 | Resolved: added width/height assertions to nil-fields-absent test | ||
| D2-9 | metadata.allium:75-77 | Resolved: 5 tests added (0→1, -5→1, 1000→500, nil→50, non-numeric→50) | |
| D2-10 | script.allium:84-88 | Resolved: 6 tests added (os.execute, os.rename, io.open for write, require, dofile, loadlib all blocked) | |
| D2-11 | script.allium:251-258 | Resolved: 2 tests already existed (per-script cap + total budget) | |
| D2-12 | task.allium:110-113 | Resolved: 2 tests added (rapid reports within 250ms dropped; value 1.0 bypasses throttle) | |
| D2-13 | post.allium:121 | Resolved: 4 tests already existed (unarchive→draft, restores content from disk, rejects non-archived, not_found) | |
| D2-14 | post.allium:122 | Resolved: test already existed (publish_post republishes archived posts without losing body or published_at) | |
| D2-15 | cli_sync.allium:64-68 | Resolved: test added (post create, media import, metadata update do not produce notification rows) | |
| D2-16 | media_processing.allium:318-343 | Resolved: validate_media/1 implemented in media.ex, 5 tests added (healthy, missing_binary, missing_sidecar, orphan, linked_not_orphan) |
|
| D2-17 | embedding.allium:199-202 | Resolved: 2 tests already existed (sync_post skips when hash matches; index_unindexed skips unchanged) |
D3. Partial Test Coverage (needs expansion)
| ID | Claim | Spec | Gap | Path |
|---|---|---|---|---|
| D3-1 | post.allium:186 | Resolved: refute published.content added to frontmatter roundtrip test |
||
| D3-2 | engine_side_effects.allium:73-74 | Resolved: test already existed (publish_post deletes old file when file path changes at posts_test.exs:284) |
||
| D3-3 | translation.allium:113 | Resolved: direct test added (do_not_translate guard rejects translation upsert with changeset error) | ||
| D3-4 | template.allium:139 | Resolved: tests already existed (publish_template rejects invalid Liquid syntax, create_and_publish rejects invalid Liquid) | ||
| D3-5 | script.allium:181 | Resolved: tests already existed (publish_script rejects invalid Lua syntax, create_and_publish rejects invalid Lua) | ||
| D3-6 | script.allium:199 | Resolved: execute_macro now returns {:ok, ""} on failure (was {:error, reason}), test updated, spec already correct |
||
| D3-7 | template.allium:53 | Resolved: roundtrip test added (publish then parse back, compare all fields against DB record) | ||
| D3-8 | metadata.allium:60 | Resolved: test added (fresh project has article/aside/page/picture categories before any operations) | ||
| D3-9 | translation.allium:178 | Resolved: test expanded (de/fr/es/it search terms all find the canonical post after reindex) | ||
| D3-10 | post.allium:33-40 | Resolved: format test added (/YYYY/MM/DD/slug/ via LinksAndLanguages.post_path) |
||
| D3-11 | post.allium:14-22 | Resolved: 5 tests added (ß→ss, ö→o, ä→a, ü→u, mixed ÄÖÜäöüß→aouaouss) |
D4. UI Test Coverage Gaps (whole-editor specs)
| ID | Spec | Covered | Not Covered |
|---|---|---|---|
mcp_rows + 3 toggle_mcp_agent tests), build_style + 4 select/change/apply/display tests), build_settings visibility tests), category_rows + 2 update + 3 add + 2 save + 4 reset tests) |
|||
| D4-3 | |||
| D4-4 | editor_script.allium | Editor layout, create defaults | |
| D4-5 | editor_template.allium | Editor layout, create defaults | |
| D4-6 | editor_tags.allium | Sync/discover, merge | |
| D4-7 | editor_misc.allium | Menu add/save, metadata diff, validation | Menu protection, import analysis, translation fix, duplicate dismiss, git diff |
Priority Order for Resolution
A1-1 through A1-15— all resolved: auto-save, on-demand preview, template lookup, validation gates, real Pagefind, graceful shutdown, real embedding model, HNSW ANN index, Apple GPU/EMLX acceleration (A1-14c), and preview/generation content strategy (A1-15) 1b.A1-16— storage-location compliance resolved: public content now lives under a per-user default content location (never the repo/private dir),priv/data/projects/<id>fallback dropped, machine-local project registry added, committed default project content removed from repo 1c.A1-17— blogmark deep-link handler resolved:BDS.Desktop.DeepLinkreceives OSbds2://URL events andBDS.Blogmarkparses them, runs the transform pipeline, and creates+opens a draft post (macOSInfo.plistscheme registration documented, pending an app-bundle pipeline)- D1-1 through D1-18 — untested invariants/guarantees
- C-1 through C-3 — internal spec inconsistencies (reconcile to code)
B1-1 through B1-20— all resolved: chat inline surfaces, auto-translation, settings sections, style tab, published snapshot fields, rendering subsystem (new rendering.allium), 404.html, media translation modal, menu ops, language picker + confirm dialog, script/template publish actions, import + documentation tabs, metadata-diff entity types, task TTL eviction, discard-post-changes, replace-media-file- A2-1 through A2-17 — spec drift (code is normative, update spec)
D2-1 through D2-17— all resolved:max_posts_per_pageconstraint, sandboxed execution, transform toast budget, progress throttle, archived→draft/published transitions, AppNoopNotifier, validate_media implementation+tests, content_hash skip on reindexD3-1 through D3-11— all resolved: content=null assertion, old-file-deletion, DNT guard, validation prerequisites (already tested), macro failure degrades to empty, template roundtrip, default categories, FTS multi-language, canonical URL format, German transliteration expansionB2-1 through B2-9— all resolved: editor_body resolver, single-post reimport, orphan import, dashboard data, missing-thumbnail regen, cache dir, stale-template prune, render labels, generation progress reporting- D4-1 through D4-3 —
UI test coverageResolved (D4-1 via standalone delete_media_translation + MediaDetectLanguage tests; D4-2 via 3 new test files + expanded managed_categories — 56 tests added; D4-3 via WelcomeScreen/CSP/chart surface tests) D4-4 through D4-6 —UI test coverageResolved (D4-4 script editor save/run/check/delete tests; D4-5 template editor save/validate/delete tests; D4-6 tag editor 17 tests covering cloud sizing, colour picker, create form, delete confirmation via overlay) D4-7 — remaining UI test coverage (menu protection, import analysis, translation fix, duplicate dismiss, git diff)