Files
bDS2/SPECGAPS.md

37 KiB
Raw Blame History

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 No 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 Publish must delete old file when path changes 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 Auto-save after 3000ms idle 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 On-demand rendering in preview server 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 lookup must use all 4 levels (post→tag→category→default) 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 17 preset colors + custom hex in tag picker 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 Template file written on create 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 Graceful shutdown with inflight request tracking 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 Real Pagefind integration for search 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 Git sidebar shows only "Working tree" placeholder 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 uses TF-IDF hash projection instead of real neural model 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 USearch HNSW ANN index + debounced persistence not implemented 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 model runs on CPU only; no Apple GPU acceleration 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 vs generation content source strategy undocumented 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 Public project content + data_path discovery not compliant with storage-location spec 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/0project_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 WYSIWYG/visual editor mode (3 modes) 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/Script are global entities 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 TagsFile uses {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 Sidecar is "YAML-like, not gray-matter" frontmatter.allium:174 Code wraps with --- delimiters Resolved: spec updated — format comment now says gray-matter style with --- delimiters
A2-5 Translation frontmatter omits status/timestamps 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 Search index has single 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 Tag archives are single-page generation.allium:142-147 Code paginates Resolved: spec updated — GenerateTagPages now paginated like categories, using max_posts_per_page
A2-8 Date archives year+month only 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 is DB entity 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 Panel tabs: problems, terminal 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 Git sidebar: commit input, history, push/pull sidebar_views.allium Only "Working tree" item Moved to A1-13: backend code exists in BDS.Git, sidebar must wire it up
A2-12 Slug timestamp fallback after 999 post.allium:21 Unbounded numeric suffix Resolved: spec updated — uniqueness comment now says unbounded numeric suffix, no 999 cap or timestamp fallback
A2-13 Thumbnail generation is async 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 AiModelModality: :video vs :file/:tool 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 JSON key convention: snake_case vs camelCase 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 Snowball stemmer language list 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
B1-1 Chat inline surfaces (9 types: card, chart, form, list, metric, mindmap, table, tabs, text/json) lib/bds/ui/chat/tool_surfaces.ex:6-15 Distill into spec
B1-2 Auto-translation system (AutoTranslation.maybe_schedule, media cascade, batch fill) lib/bds/posts/auto_translation.ex Distill into spec
B1-3 3 extra settings sections (Technology, MCP, Data Maintenance) lib/bds/ui/settings_editor/ Distill into spec
B1-4 Style/Theme as separate tab (:style), not settings section lib/bds/ui/style_editor.ex Distill into spec
B1-5 published_* snapshot fields on Post for diffing lib/bds/posts/post.ex:61-65 Add to post.allium entity
B1-6 Full rendering subsystem (Liquex, Filters, Labels, LinksAndLanguages, PostRendering) lib/bds/rendering/ Distill into spec
B1-7 404.html generation lib/bds/generation/outputs.ex:344-345 Add to 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)
B1-10 Media translation editing modal media_editor.html.heex:275-303 Add to editor_media.allium
B1-11 Menu editor drag-drop, indent/unindent/move lib/bds/desktop/menu_editor/tree_ops.ex Add to editor_misc.allium
B1-12 :language_picker overlay with flag emojis shell_overlay.html.heex:116-139 Add to modals.allium
B1-13 :confirm_dialog generic confirmation shell_overlay.html.heex:171-187 Add to modals.allium
B1-14 Publish actions for scripts and templates script_editor.html.heex:10-12, template_editor.html.heex:10-12 Add to editor_script.allium, editor_template.allium
B1-15 :import as full editor tab lib/bds/ui/import_editor.ex Add to tabs.allium
B1-16 :documentation/:api_documentation tab types lib/bds/desktop/misc_editor/ Add to tabs.allium
B1-17 Metadata diff covers embedding, media_translation, post_translation as entity types lib/bds/maintenance/repair.ex Add to metadata_diff.allium
B1-18 Finished task TTL eviction (1h, keep last 10) lib/bds/tasks.ex:365-386 Add to task.allium
B1-19 discard_post_changes/1 lib/bds/posts.ex:201-227 Add to post.allium
B1-20 replace_media_file/2 with checksum/backup lib/bds/media.ex:288-337 Add to media.allium

B2. Lower Priority (implementation detail or minor)

ID Behavior Code Location
B2-1 editor_body/1 content resolver lib/bds/posts.ex:229-252
B2-2 sync_post_from_file/1 single-post reimport lib/bds/posts.ex:254-279
B2-3 import_orphan_post_file/1 lib/bds/posts.ex:289-291
B2-4 dashboard_stats/1, post_counts_by_year_month/1 lib/bds/posts.ex:378-413
B2-5 regenerate_missing_thumbnails/2 lib/bds/media.ex:47-48
B2-6 Cache dir computation lib/bds/projects.ex:101-106
B2-7 remove_stale_published_templates lib/bds/templates.ex:524-552
B2-8 Rendering Labels module (30+ i18n strings) lib/bds/rendering/labels.ex
B2-9 Progress reporting during reindex lib/bds/generation/progress.ex

C. Internal Spec Inconsistencies

All reconciled to follow code. Specs must be self-consistent and match code.

ID Conflict Resolution Path
C-1 schema.allium ChatMessage has no cache tokens; ai.allium ChatMessage has cache_read_tokens/cache_write_tokens Code has cache tokens → align schema.allium with ai.allium Update schema.allium
C-2 media.allium SidecarFile mentions 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 translation.allium says status/timestamps omitted; frontmatter.allium TranslationFrontmatter defines only 5 fields; code writes 8+ fields 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 UniqueMediaTranslation invariant 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 UniqueTranslationPerLanguage invariant 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 BundledDefaultTemplatesExistOutsideProjectData 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 UserTemplateDirectoryOverridesBundledDefaults 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 LiquidTagSubset (5 tags only) 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 LiquidFilterSubset (4 standard + 2 custom) 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 LiquidOperatorSubset 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 MacroTimeout guarantee 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 ExecuteTransform rule (pipeline, ordering, toast budget) 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 TransformPipelineContinuation 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 ChatContextTruncation invariant 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 BoundedToolLoop enforcement 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 DiscardPostChangesSideEffects 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 ReplaceMediaFileSideEffects engine_side_effects.allium:128-134 Resolved: 3 tests added in media_test.exsreplace_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 Drag-and-drop image chain 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 ![](bds-media://id) 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 DebouncedPersistence (5s) 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 Protected categories cannot be deleted 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 HomeItemProtection (menu) editor_misc.allium:206-209 Write test: cannot move/reorder/delete Home

D2. No Test Coverage (MEDIUM priority — rules/behaviors)

ID Claim Spec Path
D2-1 RemoveCategory rule metadata.allium:100 Write test: remove category, verify list+settings+JSON updated
D2-2 CreateAndPublishTemplate rule template.allium:105 Write test: create+publish in one step
D2-3 CreateAndPublishScript rule script.allium:160 Write test: create+publish in one step
D2-4 UniqueScriptSlug dedup script.allium:115 Write test: two scripts same title → dedup slug
D2-5 FrontmatterRoundtrip invariant post.allium:223 Write test: write file, read back, assert all DB fields match
D2-6 SidecarRoundtrip invariant media.allium:198 Write test: write sidecar, read back, assert all fields match
D2-7 ConditionalPostFields: nil fields absent from frontmatter frontmatter.allium:398 Write test: post with nil excerpt/author/language → fields not in file
D2-8 ConditionalMediaFields: nil fields absent from sidecar frontmatter.allium:417 Write test: media with nil title/alt → fields not in sidecar
D2-9 max_posts_per_page 1..500 constraint metadata.allium:75-77 Write test: values outside range rejected
D2-10 SandboxedExecution: restricted capabilities blocked script.allium:84-88 Write test: filesystem/process/package loading blocked
D2-11 TransformToastBudget enforcement script.allium:251-258 Write test: per-script and total toast limits enforced
D2-12 ProgressThrottled: 250ms throttle task.allium:110-113 Write test: rapid progress reports throttled
D2-13 archived→draft transition post.allium:121 Write test: unarchive post → draft
D2-14 archived→published transition post.allium:122 Write test: unarchive post → published
D2-15 AppNoopNotifier: app writes don't produce notification rows cli_sync.allium:64-68 Write test: app mutation produces no notification row
D2-16 ValidateMedia rule media_processing.allium:318-343 Write test: missing/corrupted/orphan media detected
D2-17 ContentHashSkipsUnchanged during reindex embedding.allium:199-202 Write test: unchanged content_hash skips re-embedding

D3. Partial Test Coverage (needs expansion)

ID Claim Spec Gap Path
D3-1 PublishPost: content=null after publish post.allium:186 Not explicitly tested Add assertion
D3-2 PublishPost: old file deleted on path change engine_side_effects.allium:73-74 Not tested Add test
D3-3 UpsertPostTranslation: do_not_translate guard translation.allium:113 Indirectly covered only Add direct test
D3-4 PublishTemplate: Liquid validation prerequisite template.allium:139 Not tested as publish gate Add test
D3-5 PublishScript: validation prerequisite script.allium:181 Not tested as publish gate Add test
D3-6 ExecuteMacro failure degrades to empty script.allium:199 Returns error tuple, not empty Fix code or update spec
D3-7 TemplateFrontmatter roundtrip template.allium:53 Slug verified, no full parse-back Add roundtrip test
D3-8 DefaultCategories for fresh project metadata.allium:60 Defaults present after add, not verified fresh Add fresh-project test
D3-9 FtsIncludesTranslations translation.allium:178 Tested for one language; expand Test all stemmer languages
D3-10 PostCanonicalUrl format post.allium:33-40 Constructed in links test, not asserted as invariant Add format assertion
D3-11 Slug generation: German transliteration post.allium:14-22 "Föö Bär" → "foo-bar-blog" tested; expand ä/ö/ü/ß/ÄÖÜ Expand test

D4. UI Test Coverage Gaps (whole-editor specs)

ID Spec Covered Not Covered
D4-1 editor_media.allium AI analysis, delete Translate, replace file, link-to-post, translation CRUD, detect language
D4-2 editor_settings.allium AI endpoints, airplane toggle, rebuild Protected categories, MCP agents, style/theme, search filter, categories CRUD
D4-3 editor_chat.allium Chat creation, pinned tab API key screen, message rendering, input area, model selector, inline surfaces
D4-4 editor_script.allium Editor layout, create defaults Save, syntax check, run, delete
D4-5 editor_template.allium Editor layout, create defaults Save with validation, validate, delete with references
D4-6 editor_tags.allium Sync/discover, merge Cloud sizing, color picker, delete confirmation, create form
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

  1. 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.DeepLink receives OS bds2:// URL events and BDS.Blogmark parses them, runs the transform pipeline, and creates+opens a draft post (macOS Info.plist scheme registration documented, pending an app-bundle pipeline)
  2. D1-1 through D1-18 — untested invariants/guarantees
  3. C-1 through C-3 — internal spec inconsistencies (reconcile to code)
  4. B1-1 through B1-6 — major code behaviors missing from spec
  5. A2-1 through A2-17 — spec drift (code is normative, update spec)
  6. D2-1 through D2-17 — untested rules
  7. D3-1 through D3-11 — partial test coverage
  8. B1-7 through B1-20 — minor code behaviors missing from spec
  9. D4-1 through D4-7 — UI test coverage