chore: cleaned up some specs around extensions and small changes to get code in line
This commit is contained in:
@@ -15,7 +15,7 @@ The project is under active development. Core blogging workflows are broadly ava
|
|||||||
- Local preview in the app or system browser.
|
- Local preview in the app or system browser.
|
||||||
- Optional one-shot AI translation, description, analysis, taxonomy, and language-detection operations using online or local OpenAI-compatible endpoints with airplane-mode gating.
|
- Optional one-shot AI translation, description, analysis, taxonomy, and language-detection operations using online or local OpenAI-compatible endpoints with airplane-mode gating.
|
||||||
- SSH-agent-based SCP or rsync publishing.
|
- SSH-agent-based SCP or rsync publishing.
|
||||||
- Site, media, and translation validation plus Blogmark capture and Lua transforms.
|
- Site, media, and translation validation plus `ruds://new-post` Blogmark capture and Lua transforms; bDS2 keeps its separate `bds2://` bookmarklet protocol.
|
||||||
|
|
||||||
RuDS uses no JavaScript application runtime and loads no CSS or JavaScript from CDNs. The preview is served by the Rust application and displayed by the operating-system webview.
|
RuDS uses no JavaScript application runtime and loads no CSS or JavaScript from CDNs. The preview is served by the Rust application and displayed by the operating-system webview.
|
||||||
|
|
||||||
|
|||||||
@@ -67,12 +67,12 @@ The generated Lua documentation itself is a core requirement.
|
|||||||
|
|
||||||
Done:
|
Done:
|
||||||
|
|
||||||
- Menu file parsing/rendering, Home-first normalization, macOS URL plumbing, and Blogmark deep-link parsing.
|
- Menu file parsing/rendering and Home-first normalization.
|
||||||
|
- macOS URL plumbing and the sole bDS2-compatible Blogmark action at `ruds://new-post`; RuDS neither registers nor accepts bDS2's `bds2://` scheme.
|
||||||
|
|
||||||
Open:
|
Open:
|
||||||
|
|
||||||
- OPML/menu editor UI.
|
- OPML/menu editor UI.
|
||||||
- Remaining application deep-link parity flows.
|
|
||||||
- Replace the Menu Editor placeholder.
|
- Replace the Menu Editor placeholder.
|
||||||
|
|
||||||
### CLI, MCP, and domain events — Open
|
### CLI, MCP, and domain events — Open
|
||||||
@@ -89,12 +89,8 @@ Open:
|
|||||||
|
|
||||||
Done:
|
Done:
|
||||||
|
|
||||||
- Blogmark deep-link parsing, content capture, post import, transform selection, and Lua transform execution.
|
- Blogmark bookmarklet copy, `ruds://new-post` parsing, content capture, post import, transform selection, and Lua transform execution.
|
||||||
|
- bDS2-compatible delivery behavior without adding unsupported deep-link actions.
|
||||||
Open:
|
|
||||||
|
|
||||||
- Complete the dedicated user-facing capture/review workflow where the current implicit flow does not cover the bDS2 behavior.
|
|
||||||
- Close remaining transform-chain differences found against the specs and bDS2.
|
|
||||||
|
|
||||||
### Headless server — Open
|
### Headless server — Open
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ Diese Datei dient als Index zu allen Allium-Spezifikationen und Schema-Inventari
|
|||||||
| Datei | Scope | Status | Beschreibung |
|
| Datei | Scope | Status | Beschreibung |
|
||||||
|-------|-------|--------|--------------|
|
|-------|-------|--------|--------------|
|
||||||
| `git.allium` | Extension A | ✅ Existiert | Git-Operationen, LFS, Reconciliation |
|
| `git.allium` | Extension A | ✅ Existiert | Git-Operationen, LFS, Reconciliation |
|
||||||
|
| `import.allium` | Extension B | ✨ Neu | WordPress-WXR-Analyse, Konfliktprüfung und Importausführung |
|
||||||
| `mcp.allium` | Extension G | ✅ Existiert | MCP-Server (Tools, Resources) |
|
| `mcp.allium` | Extension G | ✅ Existiert | MCP-Server (Tools, Resources) |
|
||||||
| `ai.allium` | Core/Extension C | ✅ Existiert | AI One-Shot Tasks und Chat |
|
| `ai.allium` | Core/Extension C | ✅ Existiert | AI One-Shot Tasks und Chat |
|
||||||
| `embedding.allium` | Extension D | ✅ Existiert | Semantic Similarity (HNSW) |
|
| `embedding.allium` | Extension D | ✅ Existiert | Semantic Similarity (HNSW) |
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ const MAX_TITLE_LENGTH: usize = 200;
|
|||||||
const MAX_URL_LENGTH: usize = 2_048;
|
const MAX_URL_LENGTH: usize = 2_048;
|
||||||
const MAX_TOASTS_TOTAL: usize = 20;
|
const MAX_TOASTS_TOTAL: usize = 20;
|
||||||
const MAX_TOAST_LENGTH: usize = 300;
|
const MAX_TOAST_LENGTH: usize = 300;
|
||||||
|
const BLOGMARK_SCHEME: &str = "ruds";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct BlogmarkCandidate {
|
pub struct BlogmarkCandidate {
|
||||||
@@ -36,7 +37,7 @@ pub struct BlogmarkImportResult {
|
|||||||
pub fn parse_deep_link(raw: &str) -> EngineResult<BlogmarkCandidate> {
|
pub fn parse_deep_link(raw: &str) -> EngineResult<BlogmarkCandidate> {
|
||||||
let parsed =
|
let parsed =
|
||||||
Url::parse(raw).map_err(|_| EngineError::Validation("invalid blogmark URL".into()))?;
|
Url::parse(raw).map_err(|_| EngineError::Validation("invalid blogmark URL".into()))?;
|
||||||
if parsed.scheme() != "bds2" {
|
if parsed.scheme() != BLOGMARK_SCHEME {
|
||||||
return Err(EngineError::Validation(
|
return Err(EngineError::Validation(
|
||||||
"unsupported blogmark scheme".into(),
|
"unsupported blogmark scheme".into(),
|
||||||
));
|
));
|
||||||
@@ -66,6 +67,14 @@ pub fn parse_deep_link(raw: &str) -> EngineResult<BlogmarkCandidate> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn bookmarklet(project_id: &str) -> String {
|
||||||
|
let project_id =
|
||||||
|
url::form_urlencoded::byte_serialize(project_id.as_bytes()).collect::<String>();
|
||||||
|
format!(
|
||||||
|
"javascript:(()=>{{const t=encodeURIComponent(document.title||'');const u=encodeURIComponent(location.href||'');location.href='ruds://new-post?title='+t+'&url='+u+'&project_id={project_id}';}})();"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn receive_deep_link(
|
pub fn receive_deep_link(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
data_dir: &Path,
|
data_dir: &Path,
|
||||||
@@ -281,7 +290,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parses_and_hardens_blogmark_links() {
|
fn parses_and_hardens_blogmark_links() {
|
||||||
let candidate = parse_deep_link(
|
let candidate = parse_deep_link(
|
||||||
"bds2://new-post?title=%00Hello&url=https%3A%2F%2Fuser%3Apass%40example.com%2Fa%23frag&tags=one%2Ctwo",
|
"ruds://new-post?title=%00Hello&url=https%3A%2F%2Fuser%3Apass%40example.com%2Fa%23frag&tags=one%2Ctwo",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(candidate.title, "Hello");
|
assert_eq!(candidate.title, "Hello");
|
||||||
@@ -292,13 +301,22 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn rejects_other_schemes_and_actions() {
|
fn rejects_other_schemes_and_actions() {
|
||||||
assert!(parse_deep_link("bds://new-post?title=x").is_err());
|
assert!(parse_deep_link("bds://new-post?title=x").is_err());
|
||||||
assert!(parse_deep_link("bds2://other?title=x").is_err());
|
assert!(parse_deep_link("bds2://new-post?title=x").is_err());
|
||||||
|
assert!(parse_deep_link("ruds://other?title=x").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bookmarklet_targets_only_ruds_and_the_selected_project() {
|
||||||
|
let value = bookmarklet("project & seven");
|
||||||
|
assert!(value.contains("ruds://new-post?"));
|
||||||
|
assert!(value.contains("project_id=project+%26+seven"));
|
||||||
|
assert!(!value.contains("bds2://"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn invalid_source_url_never_reaches_candidate() {
|
fn invalid_source_url_never_reaches_candidate() {
|
||||||
let candidate =
|
let candidate =
|
||||||
parse_deep_link("bds2://new-post?title=x&url=javascript%3Aalert%281%29").unwrap();
|
parse_deep_link("ruds://new-post?title=x&url=javascript%3Aalert%281%29").unwrap();
|
||||||
assert!(candidate.url.is_none());
|
assert!(candidate.url.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,7 +346,7 @@ mod tests {
|
|||||||
db.conn(),
|
db.conn(),
|
||||||
directory.path(),
|
directory.path(),
|
||||||
&project.id,
|
&project.id,
|
||||||
"bds2://new-post?title=Example&url=https%3A%2F%2Fexample.com",
|
"ruds://new-post?title=Example&url=https%3A%2F%2Fexample.com",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,10 @@ winresource = "0.1"
|
|||||||
|
|
||||||
[package.metadata.packager]
|
[package.metadata.packager]
|
||||||
product-name = "Blogging Desktop Server"
|
product-name = "Blogging Desktop Server"
|
||||||
identifier = "de.rfc1437.bds2"
|
identifier = "de.rfc1437.ruds"
|
||||||
description = "A desktop application for writing and publishing static blogs."
|
description = "A desktop application for writing and publishing static blogs."
|
||||||
before-packaging-command = "cargo build --release -p bds-ui"
|
before-packaging-command = "cargo build --release -p bds-ui"
|
||||||
|
deep-link-protocols = [{ schemes = ["ruds"] }]
|
||||||
icons = [
|
icons = [
|
||||||
"assets/app-icons/bds.icns",
|
"assets/app-icons/bds.icns",
|
||||||
"assets/app-icons/bds.ico",
|
"assets/app-icons/bds.ico",
|
||||||
|
|||||||
@@ -4427,6 +4427,16 @@ impl BdsApp {
|
|||||||
SettingsMsg::BlogmarkCategoryChanged(s) => {
|
SettingsMsg::BlogmarkCategoryChanged(s) => {
|
||||||
state.blogmark_category = s;
|
state.blogmark_category = s;
|
||||||
}
|
}
|
||||||
|
SettingsMsg::CopyBlogmarkBookmarklet => {
|
||||||
|
if let Some(project) = &self.active_project {
|
||||||
|
let bookmarklet = engine::blogmark::bookmarklet(&project.id);
|
||||||
|
self.notify(
|
||||||
|
ToastLevel::Success,
|
||||||
|
&t(self.ui_locale, "settings.blogmarkBookmarkletCopied"),
|
||||||
|
);
|
||||||
|
return iced::clipboard::write(bookmarklet);
|
||||||
|
}
|
||||||
|
}
|
||||||
SettingsMsg::SaveProject => {
|
SettingsMsg::SaveProject => {
|
||||||
if let (Some(db), Some(data_dir), Some(project)) =
|
if let (Some(db), Some(data_dir), Some(project)) =
|
||||||
(&self.db, &self.data_dir, self.active_project.as_mut())
|
(&self.db, &self.data_dir, self.active_project.as_mut())
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ pub enum SettingsMsg {
|
|||||||
MaxPostsPerPageChanged(String),
|
MaxPostsPerPageChanged(String),
|
||||||
ImageImportConcurrencyChanged(String),
|
ImageImportConcurrencyChanged(String),
|
||||||
BlogmarkCategoryChanged(String),
|
BlogmarkCategoryChanged(String),
|
||||||
|
CopyBlogmarkBookmarklet,
|
||||||
SaveProject,
|
SaveProject,
|
||||||
// Editor
|
// Editor
|
||||||
DefaultModeChanged(String),
|
DefaultModeChanged(String),
|
||||||
@@ -543,6 +544,11 @@ fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
|
|||||||
.find(|row| row.name == state.blogmark_category),
|
.find(|row| row.name == state.blogmark_category),
|
||||||
|row| Message::Settings(SettingsMsg::BlogmarkCategoryChanged(row.name)),
|
|row| Message::Settings(SettingsMsg::BlogmarkCategoryChanged(row.name)),
|
||||||
);
|
);
|
||||||
|
let copy_blogmark_bookmarklet =
|
||||||
|
button(text(t(locale, "settings.copyBlogmarkBookmarklet")).size(13))
|
||||||
|
.on_press(Message::Settings(SettingsMsg::CopyBlogmarkBookmarklet))
|
||||||
|
.style(inputs::secondary_button)
|
||||||
|
.padding([6, 16]);
|
||||||
let save = button(text(t(locale, "common.save")).size(13))
|
let save = button(text(t(locale, "common.save")).size(13))
|
||||||
.on_press(Message::Settings(SettingsMsg::SaveProject))
|
.on_press(Message::Settings(SettingsMsg::SaveProject))
|
||||||
.style(inputs::primary_button)
|
.style(inputs::primary_button)
|
||||||
@@ -566,6 +572,7 @@ fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
|
|||||||
max_posts,
|
max_posts,
|
||||||
image_import_concurrency,
|
image_import_concurrency,
|
||||||
blogmark_category,
|
blogmark_category,
|
||||||
|
copy_blogmark_bookmarklet,
|
||||||
save,
|
save,
|
||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ fn desktop_packages_have_native_icons_and_cargo_commands() {
|
|||||||
"assets/app-icons/bds.png",
|
"assets/app-icons/bds.png",
|
||||||
"assets/app-icons/bds.ico",
|
"assets/app-icons/bds.ico",
|
||||||
"assets/app-icons/bds.icns",
|
"assets/app-icons/bds.icns",
|
||||||
|
"identifier = \"de.rfc1437.ruds\"",
|
||||||
|
"deep-link-protocols = [{ schemes = [\"ruds\"] }]",
|
||||||
"signing-identity = \"-\"",
|
"signing-identity = \"-\"",
|
||||||
] {
|
] {
|
||||||
assert!(manifest.contains(required), "missing {required}");
|
assert!(manifest.contains(required), "missing {required}");
|
||||||
|
|||||||
@@ -344,6 +344,8 @@ settings-blogLanguages = Blogsprachen
|
|||||||
settings-defaultAuthor = Standardautor
|
settings-defaultAuthor = Standardautor
|
||||||
settings-maxPostsPerPage = Beiträge pro Seite
|
settings-maxPostsPerPage = Beiträge pro Seite
|
||||||
settings-blogmarkCategory = Blogmark-Kategorie
|
settings-blogmarkCategory = Blogmark-Kategorie
|
||||||
|
settings-copyBlogmarkBookmarklet = Blogmark-Lesezeichen kopieren
|
||||||
|
settings-blogmarkBookmarkletCopied = Blogmark-Lesezeichen kopiert
|
||||||
settings-defaultMode = Standard-Editormodus
|
settings-defaultMode = Standard-Editormodus
|
||||||
settings-diffViewStyle = Diff-Ansicht
|
settings-diffViewStyle = Diff-Ansicht
|
||||||
settings-categoryTitle = Titel für { $category }
|
settings-categoryTitle = Titel für { $category }
|
||||||
|
|||||||
@@ -344,6 +344,8 @@ settings-blogLanguages = Blog Languages
|
|||||||
settings-defaultAuthor = Default Author
|
settings-defaultAuthor = Default Author
|
||||||
settings-maxPostsPerPage = Posts per Page
|
settings-maxPostsPerPage = Posts per Page
|
||||||
settings-blogmarkCategory = Blogmark Category
|
settings-blogmarkCategory = Blogmark Category
|
||||||
|
settings-copyBlogmarkBookmarklet = Copy Blogmark Bookmarklet
|
||||||
|
settings-blogmarkBookmarkletCopied = Blogmark bookmarklet copied
|
||||||
settings-defaultMode = Default Editor Mode
|
settings-defaultMode = Default Editor Mode
|
||||||
settings-diffViewStyle = Diff View Style
|
settings-diffViewStyle = Diff View Style
|
||||||
settings-categoryTitle = Title for { $category }
|
settings-categoryTitle = Title for { $category }
|
||||||
|
|||||||
@@ -344,6 +344,8 @@ settings-blogLanguages = Idiomas del blog
|
|||||||
settings-defaultAuthor = Autor predeterminado
|
settings-defaultAuthor = Autor predeterminado
|
||||||
settings-maxPostsPerPage = Artículos por página
|
settings-maxPostsPerPage = Artículos por página
|
||||||
settings-blogmarkCategory = Categoría de Blogmark
|
settings-blogmarkCategory = Categoría de Blogmark
|
||||||
|
settings-copyBlogmarkBookmarklet = Copiar bookmarklet de Blogmark
|
||||||
|
settings-blogmarkBookmarkletCopied = Bookmarklet de Blogmark copiado
|
||||||
settings-defaultMode = Modo de editor predeterminado
|
settings-defaultMode = Modo de editor predeterminado
|
||||||
settings-diffViewStyle = Estilo de diff
|
settings-diffViewStyle = Estilo de diff
|
||||||
settings-categoryTitle = Título para { $category }
|
settings-categoryTitle = Título para { $category }
|
||||||
|
|||||||
@@ -344,6 +344,8 @@ settings-blogLanguages = Langues du blog
|
|||||||
settings-defaultAuthor = Auteur par défaut
|
settings-defaultAuthor = Auteur par défaut
|
||||||
settings-maxPostsPerPage = Articles par page
|
settings-maxPostsPerPage = Articles par page
|
||||||
settings-blogmarkCategory = Catégorie Blogmark
|
settings-blogmarkCategory = Catégorie Blogmark
|
||||||
|
settings-copyBlogmarkBookmarklet = Copier le bookmarklet Blogmark
|
||||||
|
settings-blogmarkBookmarkletCopied = Bookmarklet Blogmark copié
|
||||||
settings-defaultMode = Mode d'édition par défaut
|
settings-defaultMode = Mode d'édition par défaut
|
||||||
settings-diffViewStyle = Style de diff
|
settings-diffViewStyle = Style de diff
|
||||||
settings-categoryTitle = Titre pour { $category }
|
settings-categoryTitle = Titre pour { $category }
|
||||||
|
|||||||
@@ -344,6 +344,8 @@ settings-blogLanguages = Lingue del blog
|
|||||||
settings-defaultAuthor = Autore predefinito
|
settings-defaultAuthor = Autore predefinito
|
||||||
settings-maxPostsPerPage = Articoli per pagina
|
settings-maxPostsPerPage = Articoli per pagina
|
||||||
settings-blogmarkCategory = Categoria Blogmark
|
settings-blogmarkCategory = Categoria Blogmark
|
||||||
|
settings-copyBlogmarkBookmarklet = Copia bookmarklet Blogmark
|
||||||
|
settings-blogmarkBookmarkletCopied = Bookmarklet Blogmark copiato
|
||||||
settings-defaultMode = Modalità editor predefinita
|
settings-defaultMode = Modalità editor predefinita
|
||||||
settings-diffViewStyle = Stile diff
|
settings-diffViewStyle = Stile diff
|
||||||
settings-categoryTitle = Titolo per { $category }
|
settings-categoryTitle = Titolo per { $category }
|
||||||
|
|||||||
@@ -412,7 +412,7 @@ invariant ChatCancellation {
|
|||||||
|
|
||||||
invariant StructuredRenderTools {
|
invariant StructuredRenderTools {
|
||||||
-- Chat may emit structured render payloads via tool calls prefixed "render_".
|
-- Chat may emit structured render payloads via tool calls prefixed "render_".
|
||||||
-- 9 inline surface types: card, chart, form, list, metric, mindmap, table, tabs, text, json.
|
-- 10 inline surface types: card, chart, form, list, metric, mindmap, table, tabs, text, json.
|
||||||
-- Render tool names: render_card, render_chart, render_form, render_list,
|
-- Render tool names: render_card, render_chart, render_form, render_list,
|
||||||
-- render_metric, render_mindmap, render_table, render_tabs.
|
-- render_metric, render_mindmap, render_table, render_tabs.
|
||||||
-- Unrecognized render tool names render as JSON (raw dump).
|
-- Unrecognized render tool names render as JSON (raw dump).
|
||||||
|
|||||||
@@ -48,7 +48,12 @@ use "./action_patterns.allium" as action_patterns -- AI gating, translation chai
|
|||||||
|
|
||||||
-- Integration
|
-- Integration
|
||||||
use "./git.allium" as git -- Git operations, LFS, reconciliation
|
use "./git.allium" as git -- Git operations, LFS, reconciliation
|
||||||
|
use "./import.allium" as wordpress_import -- WordPress WXR analysis and execution
|
||||||
use "./mcp.allium" as mcp -- MCP server (tools, resources, proposals)
|
use "./mcp.allium" as mcp -- MCP server (tools, resources, proposals)
|
||||||
|
use "./cli.allium" as cli -- Headless automation commands
|
||||||
|
use "./events.allium" as events -- Multi-client domain event bus
|
||||||
|
use "./server.allium" as server -- Headless SSH engine host
|
||||||
|
use "./tui.allium" as tui -- Terminal renderer and remote client
|
||||||
use "./ai.allium" as ai -- AI one-shot tasks and chat
|
use "./ai.allium" as ai -- AI one-shot tasks and chat
|
||||||
use "./embedding.allium" as embedding -- Semantic similarity (HNSW vectors)
|
use "./embedding.allium" as embedding -- Semantic similarity (HNSW vectors)
|
||||||
use "./cli_sync.allium" as cli_sync -- CLI-to-app notification sync
|
use "./cli_sync.allium" as cli_sync -- CLI-to-app notification sync
|
||||||
|
|||||||
@@ -36,9 +36,9 @@ value InlineSurface {
|
|||||||
actions: List<SurfaceAction> -- card only
|
actions: List<SurfaceAction> -- card only
|
||||||
columns: List<String> -- table
|
columns: List<String> -- table
|
||||||
rows: List<List<String>> -- table
|
rows: List<List<String>> -- table
|
||||||
chart_type: String? -- chart: bar/pie/line
|
chart_type: String? -- bar | stacked-bar | line | area | pie | donut | heatmap
|
||||||
series: List<ChartSeries> -- chart
|
series: List<ChartSeries> -- chart
|
||||||
max_value: Integer? -- chart
|
max_value: Decimal? -- chart
|
||||||
label: String? -- metric
|
label: String? -- metric
|
||||||
value: String? -- metric
|
value: String? -- metric
|
||||||
items: List<String> -- list
|
items: List<String> -- list
|
||||||
@@ -53,14 +53,19 @@ value InlineSurface {
|
|||||||
|
|
||||||
value SurfaceAction {
|
value SurfaceAction {
|
||||||
label: String
|
label: String
|
||||||
action: String
|
action: String -- navigation/view action from the allow-list below
|
||||||
payload: Map
|
payload: Map
|
||||||
}
|
}
|
||||||
|
|
||||||
value ChartSeries {
|
value ChartSeries {
|
||||||
label: String
|
label: String
|
||||||
value: Integer
|
value: Decimal
|
||||||
segments: List
|
segments: List<ChartSegment>
|
||||||
|
}
|
||||||
|
|
||||||
|
value ChartSegment {
|
||||||
|
label: String
|
||||||
|
value: Decimal
|
||||||
}
|
}
|
||||||
|
|
||||||
value MindmapNode {
|
value MindmapNode {
|
||||||
@@ -72,7 +77,7 @@ value MindmapNode {
|
|||||||
value FormField {
|
value FormField {
|
||||||
key: String
|
key: String
|
||||||
label: String
|
label: String
|
||||||
input_type: String
|
input_type: String -- text | textarea | select | checkbox | date | number
|
||||||
placeholder: String?
|
placeholder: String?
|
||||||
value: String?
|
value: String?
|
||||||
options: List<FieldOption>
|
options: List<FieldOption>
|
||||||
@@ -201,14 +206,18 @@ surface ChatPanelSurface {
|
|||||||
|
|
||||||
@guarantee AssistantActionDispatch
|
@guarantee AssistantActionDispatch
|
||||||
-- Assistant tool calls can trigger navigation actions:
|
-- Assistant tool calls can trigger navigation actions:
|
||||||
-- open_post(id), open_media(id), open_settings(), etc.
|
-- open_post(id), open_media(id), open_settings(), open_chat(id),
|
||||||
|
-- switch_view(view), toggle_sidebar(), toggle_panel(), and
|
||||||
|
-- toggle_assistant_sidebar(). Snake-case and camel-case spellings
|
||||||
|
-- are equivalent. Unknown actions or invalid identifiers are refused
|
||||||
|
-- with a visible error and never execute arbitrary code.
|
||||||
-- Actions dispatched through store, same as user clicks.
|
-- Actions dispatched through store, same as user clicks.
|
||||||
-- Navigation actions open tabs with pin intent.
|
-- Navigation actions open tabs with pin intent.
|
||||||
|
|
||||||
@guarantee InlineSurfaceRendering
|
@guarantee InlineSurfaceRendering
|
||||||
-- Assistant render-tool calls produce structured inline surfaces
|
-- Assistant render-tool calls produce structured inline surfaces
|
||||||
-- rendered between message content and tool markers.
|
-- rendered between message content and tool markers.
|
||||||
-- 9 surface types: card, chart, form, list, metric, mindmap, table, tabs, text, json.
|
-- 10 surface types: card, chart, form, list, metric, mindmap, table, tabs, text, json.
|
||||||
-- Render tool names: render_card, render_chart, render_form, render_list,
|
-- Render tool names: render_card, render_chart, render_form, render_list,
|
||||||
-- render_metric, render_mindmap, render_table, render_tabs.
|
-- render_metric, render_mindmap, render_table, render_tabs.
|
||||||
-- Unrecognized render names render as json (raw dump).
|
-- Unrecognized render names render as json (raw dump).
|
||||||
@@ -216,8 +225,24 @@ surface ChatPanelSurface {
|
|||||||
-- Form surfaces persist field values in conversation surface_state.
|
-- Form surfaces persist field values in conversation surface_state.
|
||||||
-- Tab surfaces track selected_index in conversation surface_state.
|
-- Tab surfaces track selected_index in conversation surface_state.
|
||||||
-- Surfaces can be dismissed, persisted in conversation surface_state.
|
-- Surfaces can be dismissed, persisted in conversation surface_state.
|
||||||
|
-- Stable message/tool-index IDs bind all three kinds of state to the
|
||||||
|
-- same surface when a conversation is reopened.
|
||||||
|
-- Every non-dismissed surface is expanded. Existing surfaces remain
|
||||||
|
-- visible while later assistant content and surfaces stream in.
|
||||||
|
-- Form submission adds the current field values to the action payload.
|
||||||
-- Form debounce: config.surface_form_debounce_ms.
|
-- Form debounce: config.surface_form_debounce_ms.
|
||||||
|
|
||||||
|
@guarantee ChartSurfaceParity
|
||||||
|
-- Charts support bar, stacked-bar, line, area, pie, donut, and heatmap.
|
||||||
|
-- chartType and legacy chart_type inputs are accepted; absent type
|
||||||
|
-- defaults to bar. Stacked bars and heatmaps use labelled segments.
|
||||||
|
-- Non-numeric values are rendered as zero rather than breaking chat.
|
||||||
|
|
||||||
|
@guarantee StructuredSurfaceSafety
|
||||||
|
-- Surface payloads are rendered only through the fixed component set.
|
||||||
|
-- Raw assistant HTML or JavaScript is never executed. Unknown nested
|
||||||
|
-- content renders as text or an inspectable JSON value.
|
||||||
|
|
||||||
@guarantee TokenTracking
|
@guarantee TokenTracking
|
||||||
-- Token usage tracked per conversation.
|
-- Token usage tracked per conversation.
|
||||||
-- Displayed in status bar, not in the chat panel itself.
|
-- Displayed in status bar, not in the chat panel itself.
|
||||||
|
|||||||
@@ -161,7 +161,8 @@ surface SettingsViewSurface {
|
|||||||
-- clipboard for pasting into a browser bookmark.
|
-- clipboard for pasting into a browser bookmark.
|
||||||
-- The bookmarklet captures the active tab's document.title and
|
-- The bookmarklet captures the active tab's document.title and
|
||||||
-- location.href (each URI-encoded) and navigates to a
|
-- location.href (each URI-encoded) and navigates to a
|
||||||
-- bds2://new-post?title=...&url=... deep link. When a project is active
|
-- ruds://new-post?title=...&url=... deep link. RuDS does not register or
|
||||||
|
-- accept bds2://, which remains owned by bDS2. When a project is active
|
||||||
-- its id is appended as &project_id=... so the link targets that
|
-- its id is appended as &project_id=... so the link targets that
|
||||||
-- project regardless of which one is currently open.
|
-- project regardless of which one is currently open.
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,34 @@ value GitSyncStatus {
|
|||||||
kind: local_only | remote_only | both
|
kind: local_only | remote_only | both
|
||||||
}
|
}
|
||||||
|
|
||||||
|
value GitFileStatus {
|
||||||
|
path: String
|
||||||
|
old_path: String?
|
||||||
|
kind: added | modified | deleted | renamed | untracked
|
||||||
|
}
|
||||||
|
|
||||||
|
value GitCommit {
|
||||||
|
hash: String
|
||||||
|
subject: String?
|
||||||
|
author: String?
|
||||||
|
date: String?
|
||||||
|
sync_status: GitSyncStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
value GitRemoteState {
|
||||||
|
local_branch: String?
|
||||||
|
upstream_branch: String?
|
||||||
|
has_upstream: Boolean
|
||||||
|
ahead: Integer
|
||||||
|
behind: Integer
|
||||||
|
}
|
||||||
|
|
||||||
|
config {
|
||||||
|
local_timeout: Duration = 15.seconds
|
||||||
|
network_timeout: Duration = 120.seconds
|
||||||
|
file_history_limit: Integer = 50
|
||||||
|
}
|
||||||
|
|
||||||
surface GitSyncStatusSurface {
|
surface GitSyncStatusSurface {
|
||||||
context status: GitSyncStatus
|
context status: GitSyncStatus
|
||||||
|
|
||||||
@@ -46,14 +74,24 @@ surface GitRepositorySurface {
|
|||||||
surface GitControlSurface {
|
surface GitControlSurface {
|
||||||
facing _: GitOperator
|
facing _: GitOperator
|
||||||
|
|
||||||
|
exposes:
|
||||||
|
GitFileStatus
|
||||||
|
GitCommit
|
||||||
|
GitRemoteState
|
||||||
|
|
||||||
provides:
|
provides:
|
||||||
InitializeRepoRequested(project)
|
InitializeRepoRequested(project)
|
||||||
GitStatusRequested(project)
|
GitStatusRequested(project)
|
||||||
GitDiffRequested(project)
|
GitDiffRequested(project)
|
||||||
|
GitFileDiffRequested(project, file_path)
|
||||||
GitHistoryRequested(project, branch)
|
GitHistoryRequested(project, branch)
|
||||||
|
GitFileHistoryRequested(project, file_path)
|
||||||
|
GitRemoteStateRequested(project)
|
||||||
|
GitRemoteSetRequested(project, remote_url)
|
||||||
GitFetchRequested(project)
|
GitFetchRequested(project)
|
||||||
GitPullRequested(project)
|
GitPullRequested(project)
|
||||||
GitPushRequested(project)
|
GitPushRequested(project)
|
||||||
|
GitNetworkRunCancelled(project)
|
||||||
GitCommitAllRequested(project, message)
|
GitCommitAllRequested(project, message)
|
||||||
GitReconcileRequested(project, old_commit, new_commit)
|
GitReconcileRequested(project, old_commit, new_commit)
|
||||||
PruneLfsCacheRequested(project, retain_recent)
|
PruneLfsCacheRequested(project, retain_recent)
|
||||||
@@ -85,6 +123,13 @@ rule GetDiff {
|
|||||||
ensures: GitDiffReport(staged_diff, unstaged_diff)
|
ensures: GitDiffReport(staged_diff, unstaged_diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rule GetFileDiff {
|
||||||
|
when: GitFileDiffRequested(project, file_path)
|
||||||
|
-- Returns the file at HEAD and in the working tree. A missing side is
|
||||||
|
-- represented by empty content so added and deleted files remain viewable.
|
||||||
|
ensures: GitFileDiffReport(file_path, original_content, modified_content)
|
||||||
|
}
|
||||||
|
|
||||||
rule GetHistory {
|
rule GetHistory {
|
||||||
when: GitHistoryRequested(project, branch)
|
when: GitHistoryRequested(project, branch)
|
||||||
-- Returns commit history with sync status per commit
|
-- Returns commit history with sync status per commit
|
||||||
@@ -94,9 +139,32 @@ rule GetHistory {
|
|||||||
-- This drives the "push needed" / "pull needed" indicators
|
-- This drives the "push needed" / "pull needed" indicators
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
rule GetFileHistory {
|
||||||
|
when: GitFileHistoryRequested(project, file_path)
|
||||||
|
-- Follows renames and returns the newest config.file_history_limit commits.
|
||||||
|
-- A file with no history produces an empty report rather than an error.
|
||||||
|
ensures: GitFileHistoryReport(file_path, commits)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule GetRemoteState {
|
||||||
|
when: GitRemoteStateRequested(project)
|
||||||
|
-- A repository without an upstream reports zero ahead/behind counts.
|
||||||
|
ensures: GitRemoteStateReport(project, local_branch, upstream_branch, ahead, behind)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule SetRemote {
|
||||||
|
when: GitRemoteSetRequested(project, remote_url)
|
||||||
|
-- Creates origin when absent and replaces its URL when already configured.
|
||||||
|
ensures: GitRemoteUpdated(project, remote_url)
|
||||||
|
}
|
||||||
|
|
||||||
rule Fetch {
|
rule Fetch {
|
||||||
when: GitFetchRequested(project)
|
when: GitFetchRequested(project)
|
||||||
ensures: RemoteRefsUpdated(project)
|
ensures: RemoteRefsUpdated(project)
|
||||||
|
|
||||||
|
@guidance
|
||||||
|
-- Network output is streamed live and the run can be cancelled.
|
||||||
}
|
}
|
||||||
|
|
||||||
rule Pull {
|
rule Pull {
|
||||||
@@ -104,15 +172,29 @@ rule Pull {
|
|||||||
ensures: LocalBranchUpdated(project)
|
ensures: LocalBranchUpdated(project)
|
||||||
ensures: GitReconcileRequested(project, previous_head(project), current_head(project))
|
ensures: GitReconcileRequested(project, previous_head(project), current_head(project))
|
||||||
-- After pull, detect changed files and reconcile DB
|
-- After pull, detect changed files and reconcile DB
|
||||||
|
|
||||||
|
@guidance
|
||||||
|
-- Pull is fast-forward-only. Network output is streamed live and the
|
||||||
|
-- run can be cancelled.
|
||||||
}
|
}
|
||||||
|
|
||||||
rule Push {
|
rule Push {
|
||||||
when: GitPushRequested(project)
|
when: GitPushRequested(project)
|
||||||
ensures: RemoteBranchUpdated(project)
|
ensures: RemoteBranchUpdated(project)
|
||||||
|
|
||||||
|
@guidance
|
||||||
|
-- Network output is streamed live and the run can be cancelled.
|
||||||
|
}
|
||||||
|
|
||||||
|
rule CancelNetworkRun {
|
||||||
|
when: GitNetworkRunCancelled(project)
|
||||||
|
ensures: RunningGitProcessTerminated(project)
|
||||||
|
ensures: GitNetworkRunFinished(project, cancelled)
|
||||||
}
|
}
|
||||||
|
|
||||||
rule CommitAll {
|
rule CommitAll {
|
||||||
when: GitCommitAllRequested(project, message)
|
when: GitCommitAllRequested(project, message)
|
||||||
|
requires: message != ""
|
||||||
ensures: AllChangesStaged(project)
|
ensures: AllChangesStaged(project)
|
||||||
ensures: CommitCreated(project, message)
|
ensures: CommitCreated(project, message)
|
||||||
}
|
}
|
||||||
@@ -159,6 +241,18 @@ invariant StructuredAuthErrors {
|
|||||||
-- Instead of raw git error messages
|
-- Instead of raw git error messages
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invariant BoundedGitExecution {
|
||||||
|
-- Local operations stop after config.local_timeout and network operations
|
||||||
|
-- stop after config.network_timeout. Timeout errors identify the operation
|
||||||
|
-- and preserve captured output; the spawned process is terminated.
|
||||||
|
}
|
||||||
|
|
||||||
|
invariant OfflineNetworkGate {
|
||||||
|
-- Fetch, pull, and push are refused by their UI/automation caller while
|
||||||
|
-- airplane mode is active. Repository inspection and local commits remain
|
||||||
|
-- available.
|
||||||
|
}
|
||||||
|
|
||||||
rule PruneLfsCache {
|
rule PruneLfsCache {
|
||||||
when: PruneLfsCacheRequested(project, retain_recent)
|
when: PruneLfsCacheRequested(project, retain_recent)
|
||||||
-- Prunes LFS cache with configurable recent commit retention
|
-- Prunes LFS cache with configurable recent commit retention
|
||||||
|
|||||||
291
specs/import.allium
Normal file
291
specs/import.allium
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
-- allium: 1
|
||||||
|
-- WordPress WXR Import
|
||||||
|
-- Scope: extension (saved analysis, conflict review, recoverable execution)
|
||||||
|
-- Distilled from: ../bDS2/lib/bds/wxr_parser.ex,
|
||||||
|
-- ../bDS2/lib/bds/import_analysis.ex,
|
||||||
|
-- ../bDS2/lib/bds/import_execution.ex,
|
||||||
|
-- ../bDS2/lib/bds/import_definitions.ex,
|
||||||
|
-- ../bDS2/lib/bds/desktop/shell_live/import_editor*
|
||||||
|
|
||||||
|
use "./project.allium" as project
|
||||||
|
|
||||||
|
enum ImportItemStatus { new | update | conflict | content_duplicate | missing }
|
||||||
|
enum ImportResolution { ignore | overwrite | import }
|
||||||
|
|
||||||
|
value ImportedSite {
|
||||||
|
title: String
|
||||||
|
url: String?
|
||||||
|
language: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
value ImportCandidate {
|
||||||
|
kind: post | page | media
|
||||||
|
source_id: Integer?
|
||||||
|
title: String
|
||||||
|
slug: String?
|
||||||
|
filename: String?
|
||||||
|
status: ImportItemStatus
|
||||||
|
resolution: ImportResolution?
|
||||||
|
existing_id: String?
|
||||||
|
author: String?
|
||||||
|
excerpt: String?
|
||||||
|
content: String?
|
||||||
|
source_status: String?
|
||||||
|
categories: List<String>
|
||||||
|
tags: List<String>
|
||||||
|
source_path: String?
|
||||||
|
parent_source_id: Integer?
|
||||||
|
created_at: Timestamp?
|
||||||
|
updated_at: Timestamp?
|
||||||
|
published_at: Timestamp?
|
||||||
|
}
|
||||||
|
|
||||||
|
value TaxonomyCandidate {
|
||||||
|
kind: category | tag
|
||||||
|
name: String
|
||||||
|
slug: String?
|
||||||
|
exists_in_project: Boolean
|
||||||
|
mapped_to: String?
|
||||||
|
}
|
||||||
|
|
||||||
|
value ImportCounts {
|
||||||
|
new_count: Integer
|
||||||
|
update_count: Integer
|
||||||
|
conflict_count: Integer
|
||||||
|
duplicate_count: Integer
|
||||||
|
missing_count: Integer
|
||||||
|
}
|
||||||
|
|
||||||
|
value ImportDateBucket {
|
||||||
|
year: Integer
|
||||||
|
post_count: Integer
|
||||||
|
media_count: Integer
|
||||||
|
}
|
||||||
|
|
||||||
|
value ImportMacroUsage {
|
||||||
|
name: String
|
||||||
|
total_count: Integer
|
||||||
|
post_slugs: List<String>
|
||||||
|
}
|
||||||
|
|
||||||
|
value ImportReport {
|
||||||
|
source_file: String
|
||||||
|
uploads_folder: String?
|
||||||
|
site: ImportedSite
|
||||||
|
posts: List<ImportCandidate>
|
||||||
|
pages: List<ImportCandidate>
|
||||||
|
media: List<ImportCandidate>
|
||||||
|
taxonomies: List<TaxonomyCandidate>
|
||||||
|
post_counts: ImportCounts
|
||||||
|
page_counts: ImportCounts
|
||||||
|
media_counts: ImportCounts
|
||||||
|
date_distribution: List<ImportDateBucket>
|
||||||
|
macros: List<ImportMacroUsage>
|
||||||
|
}
|
||||||
|
|
||||||
|
entity ImportDefinition {
|
||||||
|
project: project/Project
|
||||||
|
name: String
|
||||||
|
wxr_file_path: String?
|
||||||
|
uploads_folder_path: String?
|
||||||
|
last_analysis: ImportReport?
|
||||||
|
created_at: Timestamp
|
||||||
|
updated_at: Timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
config {
|
||||||
|
transaction_batch_size: Integer = 500
|
||||||
|
}
|
||||||
|
|
||||||
|
surface ImportDefinitionSurface {
|
||||||
|
context definition: ImportDefinition
|
||||||
|
|
||||||
|
exposes:
|
||||||
|
definition.project
|
||||||
|
definition.name
|
||||||
|
definition.wxr_file_path when definition.wxr_file_path != null
|
||||||
|
definition.uploads_folder_path when definition.uploads_folder_path != null
|
||||||
|
definition.last_analysis when definition.last_analysis != null
|
||||||
|
definition.created_at
|
||||||
|
definition.updated_at
|
||||||
|
}
|
||||||
|
|
||||||
|
surface WordPressImportSurface {
|
||||||
|
facing _: ImportOperator
|
||||||
|
|
||||||
|
exposes:
|
||||||
|
ImportReport
|
||||||
|
ImportCandidate
|
||||||
|
TaxonomyCandidate
|
||||||
|
|
||||||
|
provides:
|
||||||
|
CreateImportDefinitionRequested(project, name)
|
||||||
|
RenameImportDefinitionRequested(definition, name)
|
||||||
|
SelectWxrFileRequested(definition, file_path)
|
||||||
|
SelectUploadsFolderRequested(definition, folder_path)
|
||||||
|
AnalyzeWxrRequested(definition)
|
||||||
|
ResolveImportConflictRequested(definition, kind, item_name, resolution)
|
||||||
|
MapImportTaxonomyRequested(definition, kind, source_name, target_name)
|
||||||
|
AutoMapImportTaxonomyRequested(definition, model)
|
||||||
|
ExecuteWordPressImportRequested(definition)
|
||||||
|
DeleteImportDefinitionRequested(definition)
|
||||||
|
|
||||||
|
@guarantee AnalysisReview
|
||||||
|
-- Analysis runs asynchronously and reports localized progress while it
|
||||||
|
-- loads project state, analyzes posts, pages and media, processes
|
||||||
|
-- taxonomy, and discovers WordPress shortcodes. The saved report shows
|
||||||
|
-- item counts, conflicts, missing uploads, year distribution, macro
|
||||||
|
-- usage, and importable totals before any project content is changed.
|
||||||
|
|
||||||
|
@guarantee ConflictReview
|
||||||
|
-- Conflicts default to ignore. The operator can keep ignoring, overwrite
|
||||||
|
-- the existing item, or import a second item with a unique slug.
|
||||||
|
-- Same-content updates and duplicates are reported but skipped during
|
||||||
|
-- execution, matching bDS2.
|
||||||
|
|
||||||
|
@guarantee TaxonomyReview
|
||||||
|
-- Existing terms are matched case-insensitively. New source categories
|
||||||
|
-- and tags may be mapped to existing project terms, cleared, or created
|
||||||
|
-- as new terms. Free-text category mappings are normalized before save.
|
||||||
|
|
||||||
|
@guarantee AiTaxonomyGate
|
||||||
|
-- Optional automatic taxonomy mapping uses the selected configured AI
|
||||||
|
-- model. In airplane mode it runs only through a configured local
|
||||||
|
-- endpoint; otherwise it is refused with localized guidance. The saved
|
||||||
|
-- report is updated only after a successful mapping response.
|
||||||
|
|
||||||
|
@guarantee ExecutionProgress
|
||||||
|
-- Execution is unavailable when the report contains no importable
|
||||||
|
-- items. While running, the editor shows phase, completed/total count,
|
||||||
|
-- current detail and estimated remaining time for taxonomy, posts,
|
||||||
|
-- media and pages, followed by a localized result summary or error.
|
||||||
|
}
|
||||||
|
|
||||||
|
rule CreateImportDefinition {
|
||||||
|
when: CreateImportDefinitionRequested(project, name)
|
||||||
|
ensures: ImportDefinition.created(
|
||||||
|
project: project,
|
||||||
|
name: name,
|
||||||
|
wxr_file_path: null,
|
||||||
|
uploads_folder_path: null,
|
||||||
|
last_analysis: null,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule RenameImportDefinition {
|
||||||
|
when: RenameImportDefinitionRequested(definition, name)
|
||||||
|
ensures: definition.name = name
|
||||||
|
ensures: definition.updated_at = now
|
||||||
|
}
|
||||||
|
|
||||||
|
rule SelectWxrFile {
|
||||||
|
when: SelectWxrFileRequested(definition, file_path)
|
||||||
|
ensures: definition.wxr_file_path = file_path
|
||||||
|
ensures: definition.last_analysis = null
|
||||||
|
ensures: definition.updated_at = now
|
||||||
|
}
|
||||||
|
|
||||||
|
rule SelectUploadsFolder {
|
||||||
|
when: SelectUploadsFolderRequested(definition, folder_path)
|
||||||
|
ensures: definition.uploads_folder_path = folder_path
|
||||||
|
ensures: definition.updated_at = now
|
||||||
|
}
|
||||||
|
|
||||||
|
rule AnalyzeWxr {
|
||||||
|
when: AnalyzeWxrRequested(definition)
|
||||||
|
requires: definition.wxr_file_path != null
|
||||||
|
let report = analyze_wordpress_export(
|
||||||
|
definition.project,
|
||||||
|
definition.wxr_file_path,
|
||||||
|
definition.uploads_folder_path
|
||||||
|
)
|
||||||
|
ensures: definition.last_analysis = report
|
||||||
|
ensures: definition.updated_at = now
|
||||||
|
}
|
||||||
|
|
||||||
|
rule ResolveImportConflict {
|
||||||
|
when: ResolveImportConflictRequested(definition, kind, item_name, resolution)
|
||||||
|
requires: definition.last_analysis != null
|
||||||
|
requires: resolution = ignore or resolution = overwrite or resolution = import
|
||||||
|
ensures: definition.last_analysis = with_conflict_resolution(
|
||||||
|
definition.last_analysis,
|
||||||
|
kind,
|
||||||
|
item_name,
|
||||||
|
resolution
|
||||||
|
)
|
||||||
|
ensures: definition.updated_at = now
|
||||||
|
}
|
||||||
|
|
||||||
|
rule MapImportTaxonomy {
|
||||||
|
when: MapImportTaxonomyRequested(definition, kind, source_name, target_name)
|
||||||
|
requires: definition.last_analysis != null
|
||||||
|
ensures: definition.last_analysis = with_taxonomy_mapping(
|
||||||
|
definition.last_analysis,
|
||||||
|
kind,
|
||||||
|
source_name,
|
||||||
|
target_name
|
||||||
|
)
|
||||||
|
ensures: definition.updated_at = now
|
||||||
|
}
|
||||||
|
|
||||||
|
rule AutoMapImportTaxonomy {
|
||||||
|
when: AutoMapImportTaxonomyRequested(definition, model)
|
||||||
|
requires: definition.last_analysis != null
|
||||||
|
ensures: definition.last_analysis = with_ai_taxonomy_mappings(definition.last_analysis, model)
|
||||||
|
ensures: definition.updated_at = now
|
||||||
|
}
|
||||||
|
|
||||||
|
rule ExecuteWordPressImport {
|
||||||
|
when: ExecuteWordPressImportRequested(definition)
|
||||||
|
requires: definition.last_analysis != null
|
||||||
|
requires: importable_count(definition.last_analysis) > 0
|
||||||
|
ensures: WordPressImportExecuted(definition, result)
|
||||||
|
|
||||||
|
@guidance
|
||||||
|
-- New posts/pages use the source author or project default, preserve
|
||||||
|
-- source timestamps, taxonomy and published/draft status, and route
|
||||||
|
-- through normal post persistence/publishing. Pages also receive the
|
||||||
|
-- page category. Imported media route through normal media import,
|
||||||
|
-- preserve description as alt text, and link to an imported parent post
|
||||||
|
-- when its WordPress parent is known. Missing media are skipped.
|
||||||
|
}
|
||||||
|
|
||||||
|
rule DeleteImportDefinition {
|
||||||
|
when: DeleteImportDefinitionRequested(definition)
|
||||||
|
ensures: not exists definition
|
||||||
|
}
|
||||||
|
|
||||||
|
invariant WxrConversionParity {
|
||||||
|
-- A valid export requires an RSS channel and extracts site metadata, posts,
|
||||||
|
-- pages, attachments, categories and tags. Post HTML is converted to
|
||||||
|
-- Markdown and WordPress shortcodes become bDS [[macro ...]] calls.
|
||||||
|
}
|
||||||
|
|
||||||
|
invariant AnalysisClassification {
|
||||||
|
-- Posts/pages: same slug and checksum => update; same slug with different
|
||||||
|
-- content => conflict; checksum matching another post => content duplicate;
|
||||||
|
-- otherwise new. Media applies the same rules by original filename and
|
||||||
|
-- checksum, with missing selected-upload files classified as missing.
|
||||||
|
}
|
||||||
|
|
||||||
|
invariant CoreEngineReuse {
|
||||||
|
-- Execution uses the ordinary tag, post, publishing, media, filesystem,
|
||||||
|
-- search, embedding and domain-event paths. It does not create a parallel
|
||||||
|
-- import-only data model or bypass normal side effects.
|
||||||
|
}
|
||||||
|
|
||||||
|
invariant RecoverableExecution {
|
||||||
|
-- Taxonomy, posts, media and pages execute in that order. Each phase is
|
||||||
|
-- committed in batches of config.transaction_batch_size. A failure rolls
|
||||||
|
-- back the current batch and stops with a structured error; completed
|
||||||
|
-- batches remain available for inspection and a later retry.
|
||||||
|
}
|
||||||
|
|
||||||
|
invariant SafeUntrustedInput {
|
||||||
|
-- Unknown XML elements and report keys are ignored without creating
|
||||||
|
-- unbounded runtime identifiers. Malformed XML or a missing RSS channel
|
||||||
|
-- fails analysis without changing project content. Progress/reporting
|
||||||
|
-- callback failures never abort otherwise valid analysis or execution.
|
||||||
|
}
|
||||||
@@ -13,6 +13,40 @@ surface MenuManagementSurface {
|
|||||||
SyncMenuFromFilesystemRequested(project_id)
|
SyncMenuFromFilesystemRequested(project_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
surface MenuEditorSurface {
|
||||||
|
facing _: MenuOperator
|
||||||
|
|
||||||
|
provides:
|
||||||
|
MenuEditorOpened(project_id)
|
||||||
|
MenuEntryDraftRequested(menu, selected_item, kind)
|
||||||
|
MenuEntryDraftCompleted(menu, selection_or_label)
|
||||||
|
MenuEntryDraftCancelled(menu)
|
||||||
|
MenuTreeEditRequested(menu, selected_item, operation, target_item, drop_position)
|
||||||
|
UpdateMenuRequested(items)
|
||||||
|
|
||||||
|
@guarantee EntryCreation
|
||||||
|
-- A new page/category draft is inserted directly after the selected
|
||||||
|
-- item, or as the first child when a submenu is selected. A page draft
|
||||||
|
-- can select an existing page; submitting free text instead creates a
|
||||||
|
-- submenu. A category draft can select an existing category or create
|
||||||
|
-- a new category from non-empty free text. Cancelling removes the draft.
|
||||||
|
|
||||||
|
@guarantee TreeEditing
|
||||||
|
-- Entries may move among siblings, indent only beneath a preceding
|
||||||
|
-- submenu, unindent to immediately after their parent, or be dragged
|
||||||
|
-- before/after an entry or inside a submenu. A drop onto itself, one of
|
||||||
|
-- its descendants, or inside a non-submenu has no effect.
|
||||||
|
|
||||||
|
@guarantee HomeProtection
|
||||||
|
-- The canonical Home entry cannot be moved, indented, unindented,
|
||||||
|
-- deleted, or dragged. Those actions are unavailable while it is
|
||||||
|
-- selected and remain no-ops if invoked indirectly.
|
||||||
|
|
||||||
|
@guarantee SaveFeedback
|
||||||
|
-- Save persists the current normalized tree through UpdateMenu and
|
||||||
|
-- informs the operator that the blog menu was saved.
|
||||||
|
}
|
||||||
|
|
||||||
value MenuItem {
|
value MenuItem {
|
||||||
kind: page | submenu | category_archive | home
|
kind: page | submenu | category_archive | home
|
||||||
label: String
|
label: String
|
||||||
@@ -71,3 +105,30 @@ rule SyncMenuFromFilesystem {
|
|||||||
ensures: MenuLoaded(project_id, _)
|
ensures: MenuLoaded(project_id, _)
|
||||||
ensures: MenuFileWritten(_)
|
ensures: MenuFileWritten(_)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rule OpenMenuEditor {
|
||||||
|
when: MenuEditorOpened(project_id)
|
||||||
|
ensures: MenuLoaded(project_id, _)
|
||||||
|
ensures: MenuEditorReady(project_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule StartMenuEntryDraft {
|
||||||
|
when: MenuEntryDraftRequested(menu, selected_item, kind)
|
||||||
|
requires: kind = page or kind = category_archive
|
||||||
|
ensures: MenuDraftInserted(menu, selected_item, kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule CompleteMenuEntryDraft {
|
||||||
|
when: MenuEntryDraftCompleted(menu, selection_or_label)
|
||||||
|
ensures: MenuDraftResolved(menu, selection_or_label)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule CancelMenuEntryDraft {
|
||||||
|
when: MenuEntryDraftCancelled(menu)
|
||||||
|
ensures: MenuDraftRemoved(menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
rule EditMenuTree {
|
||||||
|
when: MenuTreeEditRequested(menu, selected_item, operation, target_item, drop_position)
|
||||||
|
ensures: MenuTreePreviewUpdated(menu)
|
||||||
|
}
|
||||||
|
|||||||
@@ -368,8 +368,8 @@ rule ExecuteTransform {
|
|||||||
-- config.transform_max_toast_length characters.
|
-- config.transform_max_toast_length characters.
|
||||||
|
|
||||||
@guidance
|
@guidance
|
||||||
-- bds2://new-post deep links from browser bookmarks
|
-- RuDS accepts only ruds://new-post deep links from browser bookmarks.
|
||||||
-- (bds2:// scheme avoids clashing with the legacy app's bds:// scheme)
|
-- bds2:// remains assigned to bDS2 so both applications can be installed.
|
||||||
-- Ordering is deterministic: updated_at, then slug, then id
|
-- Ordering is deterministic: updated_at, then slug, then id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user