37 KiB
37 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 |
|---|---|---|---|
| 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 | 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 |
|---|---|---|---|
| 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
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-6 — major code behaviors missing from spec
- A2-1 through A2-17 — spec drift (code is normative, update spec)
- D2-1 through D2-17 — untested rules
- D3-1 through D3-11 — partial test coverage
- B1-7 through B1-20 — minor code behaviors missing from spec
- D4-1 through D4-7 — UI test coverage