diff --git a/docs/INTERFACE_PATTERN_MIGRATION.md b/docs/INTERFACE_PATTERN_MIGRATION.md
new file mode 100644
index 0000000..b47cf66
--- /dev/null
+++ b/docs/INTERFACE_PATTERN_MIGRATION.md
@@ -0,0 +1,38 @@
+# Dataflow Interface Pattern Migration
+
+This migration applies the GovOPlaN interface pattern language to the pipeline
+library, graphical and constrained-SQL editors, node inspector, intermediate
+preview, automation triggers, and durable run/deployment surfaces.
+
+## Surface Inventory
+
+| Surface | Archetype | Consequence class | Contract |
+| --- | --- | --- | --- |
+| `/dataflow` library | Governed directory | Select, create, reuse, or retire a pipeline | Stable loading, empty, permission, read-only, disabled-reason, and help states |
+| Graph/SQL workspace | Consequential definition editor | Append immutable revision | One canonical graph, guarded draft, constrained SQL compilation, diagnostics, and explicit save/discard |
+| Node inspector | Typed configuration editor | Change transform semantics | Contextual node/expression help, typed validation, bounded intermediate preview, and read-only state |
+| Trigger editor | Governed automation editor | Create, enable, change, or delete trigger | Guarded nested draft, pinned revision/grant, authorization recheck, and destructive confirmation |
+| Preview/results | Bounded evidence preview | Read transient intermediate rows | Selected-node context, diagnostics, privacy boundary, row limit, and no retained preview contents |
+| Runs/deployments | Asynchronous command register | Queue, publish, cancel, reconcile, or promote | Explicit consequence confirmation, durable command identity, progress, recovery state, and retained evidence |
+
+## Consequence And Availability Rules
+
+- Saving appends an immutable definition revision. Derivation creates a
+ separately governed copy pinned to the source revision and content hash.
+- SQL is parsed and compiled into the canonical graph. It is never passed
+ directly to a database or execution provider.
+- Preview is bounded and transient. A saved run is revision-pinned and records
+ actor, authority, idempotency, progress, output, and recovery evidence.
+- Publishing appends a governed Datasource materialization. Unknown external
+ outcomes require reconciliation before retry.
+- Staging and production promotion require confirmation and never rewrite a
+ revision. Cancellation cannot promise reversal of acknowledged external
+ effects.
+- Missing optional Datasources or automation capabilities disable only their
+ associated actions; local graph, SQL, validation, and preview behavior stays
+ available where authorized.
+
+Backend and WebUI manifests publish matching surface identifiers. English and
+German catalogues cover module-owned navigation, states, and core commands.
+Contextual help resolves from manifest documentation, main and nested drafts
+are guarded, and unavailable actions carry stable reasons.
diff --git a/src/govoplan_dataflow/backend/manifest.py b/src/govoplan_dataflow/backend/manifest.py
index c2ab4ca..81938d0 100644
--- a/src/govoplan_dataflow/backend/manifest.py
+++ b/src/govoplan_dataflow/backend/manifest.py
@@ -181,6 +181,113 @@ DOCUMENTATION = (
"Output publication uses forward recovery and blocks blind retry "
"when provider acknowledgement is uncertain."
),
+ "help_contexts": [
+ "dataflow.page",
+ "dataflow.library",
+ "dataflow.graph",
+ "dataflow.sql",
+ "dataflow.inspector",
+ "dataflow.results",
+ "dataflow.state.read-only",
+ ],
+ },
+ ),
+ DocumentationTopic(
+ id="dataflow.reference.nodes-and-expressions",
+ title="Dataflow nodes and expressions",
+ summary="Typed node inputs, expressions, schema propagation, and bounded intermediate previews.",
+ body=(
+ "Every graph node declares typed inputs, configuration, output schema, and validation rules. "
+ "Source nodes pin inline content or governed Datasource references; combine, filter, transform, "
+ "quality, reconciliation, reusable-subflow, and output nodes remain explicit in the canonical graph. "
+ "Expressions use the typed Dataflow expression language and never execute arbitrary host or database "
+ "code. Selecting a node may request a bounded intermediate preview; preview rows are transient, "
+ "privacy-filtered for the actor, and are not retained as run output. SQL editing compiles into the same "
+ "canonical graph, so unsupported statements are diagnostics rather than pass-through SQL."
+ ),
+ layer="available",
+ documentation_types=("admin", "user"),
+ audience=("operator", "module_admin", "power_user", "data_steward"),
+ order=76,
+ related_modules=("datasources", "connectors", "policy", "audit"),
+ metadata={
+ "help_contexts": [
+ "dataflow.field.node-name",
+ "dataflow.field.source",
+ "dataflow.field.expression",
+ "dataflow.field.schema",
+ "dataflow.action.preview-node",
+ ],
+ },
+ ),
+ DocumentationTopic(
+ id="dataflow.reference.fields-and-consequences",
+ title="Dataflow fields and lifecycle consequences",
+ summary="Definition scope, revision, reuse, automation, execution, publication, promotion, and deletion semantics.",
+ body=(
+ "Scope determines ownership and Policy inheritance. Templates can be derived but not run; complete "
+ "flows may be previewed, revisioned, automated, and executed when effective Policy allows it. Saving "
+ "appends an immutable revision. A scoped copy pins its source revision and content hash. Triggers pin "
+ "the revision and authorization grant, then re-evaluate authority for every delivery. Runs create "
+ "durable command and recovery evidence. Publishing creates a governed Datasource materialization, and "
+ "environment promotion changes which immutable revision is eligible for staging or production runs. "
+ "Deletion prevents future use while retained run, deployment, lineage, audit, and recovery evidence "
+ "continues under its retention policy."
+ ),
+ layer="available",
+ documentation_types=("admin", "user"),
+ audience=("operator", "module_admin", "power_user", "product_owner"),
+ order=77,
+ related_modules=("datasources", "workflow_engine", "notifications", "policy", "audit"),
+ metadata={
+ "help_contexts": [
+ "dataflow.field.scope",
+ "dataflow.field.definition-kind",
+ "dataflow.action.save",
+ "dataflow.action.derive",
+ "dataflow.action.trigger",
+ "dataflow.action.delete",
+ ],
+ "consequence_classes": {
+ "save_revision": "Appends an immutable pipeline definition revision.",
+ "derive_copy": "Creates a separately governed copy pinned to the source revision and hash.",
+ "configure_trigger": "Creates or changes an automation command with revision and authorization evidence.",
+ "delete_pipeline": "Prevents future use while retained evidence remains governed.",
+ },
+ },
+ ),
+ DocumentationTopic(
+ id="dataflow.execution-and-recovery",
+ title="Dataflow execution, publication, and recovery",
+ summary="Pinned runs, environment promotion, output publication, cancellation, reconciliation, and retained evidence.",
+ body=(
+ "Every run is pinned to an immutable revision and idempotency key. The queue records actor, authority, "
+ "environment, progress, cancellation, output, and recovery state. Database-only runs commit atomically. "
+ "Publication to a governed Datasource uses forward recovery: an unknown provider outcome is reconciled "
+ "before retry so output is not duplicated. Staging and production promotion is explicit and does not "
+ "rewrite a revision. Cancellation is best effort once external work has started; the final evidence "
+ "states whether work stopped, completed, failed, or requires operator reconciliation."
+ ),
+ layer="available",
+ documentation_types=("admin", "user"),
+ audience=("operator", "module_admin", "power_user", "security_admin"),
+ order=78,
+ related_modules=("datasources", "notifications", "policy", "audit", "ops"),
+ metadata={
+ "help_contexts": [
+ "dataflow.runs",
+ "dataflow.action.queue-run",
+ "dataflow.action.publish",
+ "dataflow.action.promote-staging",
+ "dataflow.action.promote-production",
+ "dataflow.state.recovery-attention",
+ ],
+ "consequence_classes": {
+ "queue_run": "Creates a durable asynchronous command and authorization evidence.",
+ "publish_output": "Creates or updates a governed Datasource and appends a materialization.",
+ "promote_revision": "Makes an immutable revision eligible in a higher execution environment.",
+ "cancel_run": "Requests cancellation; already acknowledged external effects may remain.",
+ },
},
),
)
@@ -374,6 +481,69 @@ manifest = ModuleManifest(
),
),
view_surfaces=(
+ ViewSurface(
+ id="dataflow.page",
+ module_id=MODULE_ID,
+ kind="route",
+ label="Dataflow",
+ order=72,
+ ),
+ ViewSurface(
+ id="dataflow.library",
+ module_id=MODULE_ID,
+ kind="section",
+ label="Pipeline library",
+ parent_id="dataflow.page",
+ order=10,
+ ),
+ ViewSurface(
+ id="dataflow.graph",
+ module_id=MODULE_ID,
+ kind="section",
+ label="Graph editor",
+ parent_id="dataflow.page",
+ order=20,
+ ),
+ ViewSurface(
+ id="dataflow.sql",
+ module_id=MODULE_ID,
+ kind="section",
+ label="Constrained SQL editor",
+ parent_id="dataflow.page",
+ order=30,
+ ),
+ ViewSurface(
+ id="dataflow.inspector",
+ module_id=MODULE_ID,
+ kind="section",
+ label="Node inspector",
+ parent_id="dataflow.page",
+ order=40,
+ ),
+ ViewSurface(
+ id="dataflow.results",
+ module_id=MODULE_ID,
+ kind="section",
+ label="Preview and diagnostics",
+ parent_id="dataflow.page",
+ order=50,
+ ),
+ ViewSurface(
+ id="dataflow.triggers",
+ module_id=MODULE_ID,
+ kind="action",
+ label="Automation triggers",
+ parent_id="dataflow.page",
+ order=60,
+ ),
+ ViewSurface(
+ id="dataflow.runs",
+ module_id=MODULE_ID,
+ kind="action",
+ label="Runs and deployments",
+ parent_id="dataflow.page",
+ order=70,
+ ),
ViewSurface(
id="dataflow.widget.pipelines",
module_id=MODULE_ID,
diff --git a/tests/test_interface_documentation_contract.py b/tests/test_interface_documentation_contract.py
new file mode 100644
index 0000000..b429e3b
--- /dev/null
+++ b/tests/test_interface_documentation_contract.py
@@ -0,0 +1,75 @@
+from __future__ import annotations
+
+from pathlib import Path
+import unittest
+
+from govoplan_dataflow.backend.manifest import get_manifest
+
+
+REPO_ROOT = Path(__file__).resolve().parents[1]
+
+
+class DataflowInterfaceDocumentationContractTests(unittest.TestCase):
+ def test_backend_surfaces_and_hierarchy_remain_declared(self) -> None:
+ frontend = get_manifest().frontend
+ self.assertIsNotNone(frontend)
+ surfaces = {item.id: item for item in frontend.view_surfaces} # type: ignore[union-attr]
+ self.assertEqual(
+ {
+ "dataflow.page",
+ "dataflow.library",
+ "dataflow.graph",
+ "dataflow.sql",
+ "dataflow.inspector",
+ "dataflow.results",
+ "dataflow.triggers",
+ "dataflow.runs",
+ "dataflow.widget.pipelines",
+ },
+ set(surfaces),
+ )
+ for surface_id in (
+ "dataflow.library",
+ "dataflow.graph",
+ "dataflow.sql",
+ "dataflow.inspector",
+ "dataflow.results",
+ "dataflow.triggers",
+ "dataflow.runs",
+ ):
+ self.assertEqual("dataflow.page", surfaces[surface_id].parent_id)
+
+ def test_help_and_consequence_metadata_remain_published(self) -> None:
+ topics = {topic.id: topic for topic in get_manifest().documentation}
+ boundary = topics["dataflow.module-boundary"]
+ fields = topics["dataflow.reference.fields-and-consequences"]
+ nodes = topics["dataflow.reference.nodes-and-expressions"]
+ execution = topics["dataflow.execution-and-recovery"]
+
+ self.assertIn("dataflow.state.read-only", boundary.metadata["help_contexts"])
+ self.assertIn("dataflow.field.expression", nodes.metadata["help_contexts"])
+ self.assertIn("save_revision", fields.metadata["consequence_classes"])
+ self.assertIn("delete_pipeline", fields.metadata["consequence_classes"])
+ self.assertIn("publish_output", execution.metadata["consequence_classes"])
+ self.assertIn("promote_revision", execution.metadata["consequence_classes"])
+
+ def test_webui_uses_shared_help_guard_and_consequence_components(self) -> None:
+ page = (REPO_ROOT / "webui/src/features/dataflow/DataflowPage.tsx").read_text(
+ encoding="utf-8"
+ )
+ inspector = (
+ REPO_ROOT / "webui/src/features/dataflow/NodeInspector.tsx"
+ ).read_text(encoding="utf-8")
+
+ for component in (
+ "ActionBlockerHint",
+ "DocumentationHelpLink",
+ "useUnsavedDraftGuard",
+ "ConfirmDialog",
+ ):
+ self.assertIn(component, page)
+ self.assertIn("DATAFLOW_NODE_DOCUMENTATION", inspector)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/webui/src/features/dataflow/DataflowPage.tsx b/webui/src/features/dataflow/DataflowPage.tsx
index 365409b..aac1f1d 100644
--- a/webui/src/features/dataflow/DataflowPage.tsx
+++ b/webui/src/features/dataflow/DataflowPage.tsx
@@ -26,10 +26,12 @@ import {
Upload
} from "lucide-react";
import {
+ ActionBlockerHint,
Button,
ConfirmDialog,
Dialog,
DismissibleAlert,
+ DocumentationHelpLink,
FormField,
IconButton,
LoadingFrame,
@@ -97,6 +99,12 @@ import {
type PipelineDraft
} from "./model";
import { dataflowNodeIcon } from "./nodeIcons";
+import {
+ DATAFLOW_DOCUMENTATION,
+ DATAFLOW_FIELDS_DOCUMENTATION,
+ DATAFLOW_I18N,
+ DATAFLOW_RUN_DOCUMENTATION
+} from "./interfacePatterns";
type ResultTab = "preview" | "diagnostics";
type SnapshotFormat = "json" | "csv";
@@ -145,6 +153,9 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
const canEdit = canWrite && (
!draft?.id || draft.governance?.actions.edit?.allowed !== false
);
+ const editBlockedReason = !canWrite
+ ? DATAFLOW_I18N.writeReason
+ : draft?.governance?.actions.edit?.reason ?? DATAFLOW_I18N.writeReason;
const canReuse = Boolean(
draft?.id
&& canWrite
@@ -313,8 +324,8 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
useUnsavedDraftGuard({
dirty,
- title: "Unsaved pipeline",
- message: "Save or discard the pipeline changes before leaving this workspace.",
+ title: "i18n:govoplan-dataflow.unsaved_title",
+ message: "i18n:govoplan-dataflow.unsaved_message",
onSave: saveDraft,
onDiscard: discardDraft
});
@@ -577,6 +588,7 @@ export default function DataflowPage({ settings, auth }: { settings: ApiSettings
variant="ghost"
onClick={() => requestNavigation(() => void loadPipelines(draft?.id))}
disabled={loading}
+ disabledReason={loading ? DATAFLOW_I18N.loading : undefined}
/>