chore: cleaned up some specs around extensions and small changes to get code in line

This commit is contained in:
2026-07-18 22:52:54 +02:00
parent 5c1b333cce
commit a2e2ab037d
21 changed files with 549 additions and 27 deletions

View File

@@ -412,7 +412,7 @@ invariant ChatCancellation {
invariant StructuredRenderTools {
-- 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_metric, render_mindmap, render_table, render_tabs.
-- Unrecognized render tool names render as JSON (raw dump).

View File

@@ -48,7 +48,12 @@ use "./action_patterns.allium" as action_patterns -- AI gating, translation chai
-- Integration
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 "./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 "./embedding.allium" as embedding -- Semantic similarity (HNSW vectors)
use "./cli_sync.allium" as cli_sync -- CLI-to-app notification sync

View File

@@ -36,9 +36,9 @@ value InlineSurface {
actions: List<SurfaceAction> -- card only
columns: 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
max_value: Integer? -- chart
max_value: Decimal? -- chart
label: String? -- metric
value: String? -- metric
items: List<String> -- list
@@ -53,14 +53,19 @@ value InlineSurface {
value SurfaceAction {
label: String
action: String
action: String -- navigation/view action from the allow-list below
payload: Map
}
value ChartSeries {
label: String
value: Integer
segments: List
value: Decimal
segments: List<ChartSegment>
}
value ChartSegment {
label: String
value: Decimal
}
value MindmapNode {
@@ -72,7 +77,7 @@ value MindmapNode {
value FormField {
key: String
label: String
input_type: String
input_type: String -- text | textarea | select | checkbox | date | number
placeholder: String?
value: String?
options: List<FieldOption>
@@ -201,14 +206,18 @@ surface ChatPanelSurface {
@guarantee AssistantActionDispatch
-- 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.
-- Navigation actions open tabs with pin intent.
@guarantee InlineSurfaceRendering
-- Assistant render-tool calls produce structured inline surfaces
-- 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_metric, render_mindmap, render_table, render_tabs.
-- Unrecognized render names render as json (raw dump).
@@ -216,8 +225,24 @@ surface ChatPanelSurface {
-- Form surfaces persist field values in conversation surface_state.
-- Tab surfaces track selected_index 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.
@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
-- Token usage tracked per conversation.
-- Displayed in status bar, not in the chat panel itself.

View File

@@ -161,7 +161,8 @@ surface SettingsViewSurface {
-- clipboard for pasting into a browser bookmark.
-- The bookmarklet captures the active tab's document.title and
-- 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
-- project regardless of which one is currently open.

View File

@@ -17,6 +17,34 @@ value GitSyncStatus {
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 {
context status: GitSyncStatus
@@ -46,14 +74,24 @@ surface GitRepositorySurface {
surface GitControlSurface {
facing _: GitOperator
exposes:
GitFileStatus
GitCommit
GitRemoteState
provides:
InitializeRepoRequested(project)
GitStatusRequested(project)
GitDiffRequested(project)
GitFileDiffRequested(project, file_path)
GitHistoryRequested(project, branch)
GitFileHistoryRequested(project, file_path)
GitRemoteStateRequested(project)
GitRemoteSetRequested(project, remote_url)
GitFetchRequested(project)
GitPullRequested(project)
GitPushRequested(project)
GitNetworkRunCancelled(project)
GitCommitAllRequested(project, message)
GitReconcileRequested(project, old_commit, new_commit)
PruneLfsCacheRequested(project, retain_recent)
@@ -85,6 +123,13 @@ rule GetDiff {
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 {
when: GitHistoryRequested(project, branch)
-- Returns commit history with sync status per commit
@@ -94,9 +139,32 @@ rule GetHistory {
-- 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 {
when: GitFetchRequested(project)
ensures: RemoteRefsUpdated(project)
@guidance
-- Network output is streamed live and the run can be cancelled.
}
rule Pull {
@@ -104,15 +172,29 @@ rule Pull {
ensures: LocalBranchUpdated(project)
ensures: GitReconcileRequested(project, previous_head(project), current_head(project))
-- 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 {
when: GitPushRequested(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 {
when: GitCommitAllRequested(project, message)
requires: message != ""
ensures: AllChangesStaged(project)
ensures: CommitCreated(project, message)
}
@@ -159,6 +241,18 @@ invariant StructuredAuthErrors {
-- 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 {
when: PruneLfsCacheRequested(project, retain_recent)
-- Prunes LFS cache with configurable recent commit retention

291
specs/import.allium Normal file
View 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.
}

View File

@@ -13,6 +13,40 @@ surface MenuManagementSurface {
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 {
kind: page | submenu | category_archive | home
label: String
@@ -71,3 +105,30 @@ rule SyncMenuFromFilesystem {
ensures: MenuLoaded(project_id, _)
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)
}

View File

@@ -368,8 +368,8 @@ rule ExecuteTransform {
-- config.transform_max_toast_length characters.
@guidance
-- bds2://new-post deep links from browser bookmarks
-- (bds2:// scheme avoids clashing with the legacy app's bds:// scheme)
-- RuDS accepts only ruds://new-post deep links from browser bookmarks.
-- bds2:// remains assigned to bDS2 so both applications can be installed.
-- Ordering is deterministic: updated_at, then slug, then id
}