diff --git a/SPECGAPS.md b/SPECGAPS.md index 03231c8..6a12a7a 100644 --- a/SPECGAPS.md +++ b/SPECGAPS.md @@ -61,7 +61,7 @@ Gap categories: **SC** = spec correct, fix code | **CS** = code correct, update | ID | Behavior | Code Location | Path | |---|---|---|---| | ~~B1-1~~ | ~~Chat inline surfaces (9 types: card, chart, form, list, metric, mindmap, table, tabs, text/json)~~ | `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 | -| B1-2 | Auto-translation system (AutoTranslation.maybe_schedule, media cascade, batch fill) | `lib/bds/posts/auto_translation.ex` | Distill into spec | +| ~~B1-2~~ | ~~Auto-translation system (AutoTranslation.maybe_schedule, media cascade, batch fill)~~ | `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 | | 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 | @@ -193,7 +193,7 @@ All reconciled to follow code. Specs must be self-consistent and match code. 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 +4. **B1-1 through B1-6** — major code behaviors missing from spec (B1-1, B1-2 resolved) 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 diff --git a/specs/translation.allium b/specs/translation.allium index a4a168f..a21b0f4 100644 --- a/specs/translation.allium +++ b/specs/translation.allium @@ -186,3 +186,128 @@ invariant FtsIncludesTranslations { for t in post.translations: includes_text(search_index(post), t.title) } + +-- =========================================================================== +-- Auto-Translation System +-- Distilled from: lib/bds/posts/auto_translation.ex +-- +-- Two entry points share one translation primitive: +-- 1. ScheduleAutoTranslation - reactive, fired after a post is created or +-- updated. One background task per missing language produces a DRAFT +-- translation, then cascades to the post's linked media. +-- 2. FillMissingTranslations - batch maintenance action. Scans every +-- published post, AUTO-PUBLISHES the generated translations, fills linked +-- media, reports progress and returns a summary. +-- All AI work is gated by a resolvable endpoint and runs on background tasks. +-- =========================================================================== + +config { + -- Background translations use the "AI" task group named per project. + auto_translation_task_group_name: String = "AI" +} + +surface AutoTranslationControlSurface { + facing _: TranslationOperator + + provides: + -- Reactive trigger: emitted by post create/update side effects. + PostSavedForAutoTranslation(post) + -- Batch trigger: "fill missing translations" maintenance action. + FillMissingTranslationsRequested(project) +} + +invariant AutoTranslationGatedByEndpoint { + -- No automatic translation runs unless an endpoint is resolvable for the + -- current mode. Airplane mode needs url+model; online additionally needs an + -- api_key. When unconfigured, scheduling is a silent no-op. + -- See ai.allium AirplaneModeGating for endpoint selection. + for post in Posts: + auto_translation_runs(post) implies endpoint_configured(post.project) +} + +invariant AutoTranslationSkipsDoNotTranslate { + -- Posts flagged do_not_translate never schedule background translation, + -- and the batch scan rejects them before computing missing languages. + for post in Posts where post.do_not_translate: + not auto_translation_runs(post) +} + +invariant AutoTranslationOnlyMissingLanguages { + -- The target set is the configured languages (main_language plus + -- blog_languages, normalized + de-duplicated) minus the post's source + -- language and any language that already has a translation. + for post in Posts: + auto_translation_targets(post) = + configured_languages(post.project) + - source_language(post) + - post.available_languages +} + +rule ScheduleAutoTranslation { + when: PostSavedForAutoTranslation(post) + requires: not post.do_not_translate + requires: endpoint_configured(post.project) + -- One background task per missing language, each producing a DRAFT + -- translation (not auto-published) followed by a media cascade. + for language in auto_translation_targets(post): + ensures: BackgroundTaskSubmitted( + group: post.project, + group_name: config.auto_translation_task_group_name) + ensures: AutoTranslatePost(post, language, auto_publish: false) + ensures: AutoTranslateMediaCascade(post, language) + + @guidance + -- Best-effort: missing metadata, unconfigured endpoint, or + -- do_not_translate all collapse to a silent success with no task. +} + +rule AutoTranslatePost { + when: AutoTranslatePost(post, language, auto_publish) + requires: trim(editor_body(post)) != "" + -- Calls the AI endpoint with the post's source language, then upserts a + -- translation marked auto_generated. Publishes only in the batch path. + ensures: + let translation = UpsertPostTranslation(post, language) + if auto_publish: + translation.status = published + else: + translation.status = draft + + @guidance + -- An empty body yields a no_content_to_translate error and no + -- translation is created. +} + +rule AutoTranslateMediaCascade { + when: AutoTranslateMediaCascade(post, language) + -- After a post translation, each linked media (ordered by sort_order) gets + -- its own background task when its source language differs from the target + -- and it lacks a translation in that language. + for m in post.linked_media: + if m.language != "" and m.language != language and not (language in m.available_languages): + ensures: BackgroundTaskSubmitted( + group: post.project, + group_name: config.auto_translation_task_group_name) + ensures: media/UpsertMediaTranslation(m, language) +} + +rule FillMissingTranslations { + when: FillMissingTranslationsRequested(project) + -- Batch maintenance. No-op (nothing_to_do) when there is at most one + -- configured language or nothing is missing. Otherwise scans published, + -- non-do_not_translate posts and their linked media. + requires: configured_languages(project).count > 1 + for post in project.posts where status = published and not do_not_translate: + for language in auto_translation_targets(post): + ensures: AutoTranslatePost(post, language, auto_publish: true) + ensures: AutoTranslateMediaCascade(post, language) + ensures: ProgressReported(project) + ensures: FillMissingTranslationsCompleted(project) + + @guidance + -- Returns a summary of counts: translated_posts, translated_media, + -- failed_count, warned_count, and nothing_to_do. nothing_to_do is true + -- when there is at most one configured language or nothing is missing. + -- Per-item failures increment failed_count and never abort the batch. + -- Progress runs through scanning (0.0-0.15) then per-item (0.15-1.0). +}