diff --git a/contracts/workflow-api.manifest.json b/contracts/workflow-api.manifest.json index 8bf4f81f..152450cb 100644 --- a/contracts/workflow-api.manifest.json +++ b/contracts/workflow-api.manifest.json @@ -218,6 +218,45 @@ ], "type": "string" }, + "ArtifactPlanDiagnosticPayload": { + "description": "Stable diagnostic projected when an artifact plan is invalid.", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + }, + "path": { + "type": "string" + }, + "repair_hint": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "severity": { + "enum": [ + "error", + "warning" + ], + "type": "string" + } + }, + "required": [ + "severity", + "code", + "path", + "message", + "repair_hint" + ], + "type": "object" + }, "AuthRecordSummaryPayload": { "additionalProperties": false, "description": "Auth record summary without credential payload values.\n\n``metadata`` is explicitly non-secret display data. Credential material\nbelongs in the omitted auth payload and is represented only by key names.", @@ -4387,6 +4426,43 @@ ], "type": "object" }, + "ValidateArtifactPlanResult": { + "description": "Non-persisting artifact-plan validation and dependency inventory.", + "properties": { + "diagnostics": { + "items": { + "$ref": "#/components/schemas/ArtifactPlanDiagnosticPayload" + }, + "type": "array" + }, + "required_capabilities": { + "items": { + "$ref": "#/components/schemas/RequiredCapabilityPayload" + }, + "type": "array" + }, + "status": { + "enum": [ + "valid", + "invalid" + ], + "type": "string" + }, + "workflow_dependencies": { + "additionalProperties": { + "type": "integer" + }, + "type": "object" + } + }, + "required": [ + "status", + "diagnostics", + "required_capabilities", + "workflow_dependencies" + ], + "type": "object" + }, "ValidateDeploymentResult": { "properties": { "artifact_id": { @@ -5714,6 +5790,81 @@ } } }, + { + "action": "validate_plan", + "errors": [ + { + "$ref": "#/components/errors/5000" + } + ], + "method": "workflow.artifacts.validate_plan", + "namespace": [ + "workflow", + "artifacts" + ], + "params": [ + { + "name": "plan", + "required": true, + "schema": { + "additionalProperties": true, + "type": "object" + } + }, + { + "name": "outcomes", + "required": true, + "schema": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + { + "name": "required_capabilities", + "required": false, + "schema": { + "anyOf": [ + { + "additionalProperties": { + "additionalProperties": true, + "type": "object" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null + } + }, + { + "name": "source_bindings", + "required": false, + "schema": { + "anyOf": [ + { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": null + } + } + ], + "result": { + "schema": { + "$ref": "#/components/schemas/ValidateArtifactPlanResult" + } + } + }, { "action": "call", "errors": [ diff --git a/src/wf_api/artifacts.py b/src/wf_api/artifacts.py index 97c1f9f9..ad37d09b 100644 --- a/src/wf_api/artifacts.py +++ b/src/wf_api/artifacts.py @@ -9,6 +9,8 @@ from __future__ import annotations from collections.abc import Mapping, Sequence from typing import Any +from pydantic import ValidationError + from wf_artifacts import ( ArtifactKind, RequiredCapability, @@ -25,6 +27,7 @@ from .capability_requirements import observed_node_specs from .drafts import WorkflowDraftApi from .listing import matches_query, paged_list_payload from .models import ( + ArtifactPlanDiagnosticPayload, CreateArtifactFromWorkspaceResult, DeleteArtifactResult, JsonProjector, @@ -33,6 +36,7 @@ from .models import ( SaveArtifactResult, SavedDraftArtifactResult, UnsavedDraftArtifactResult, + ValidateArtifactPlanResult, WorkflowArtifactPayload, ) from .operation_context import WorkflowOperationContext @@ -41,10 +45,76 @@ _PROJECT_ARTIFACT = JsonProjector(WorkflowArtifactPayload) _PROJECT_ARTIFACT_LIST = JsonProjector(ListArtifactsResult) _PROJECT_ARTIFACT_SAVE = JsonProjector(SaveArtifactResult) _PROJECT_ARTIFACT_DELETE = JsonProjector(DeleteArtifactResult) +_PROJECT_VALIDATE_ARTIFACT = JsonProjector(ValidateArtifactPlanResult) _PROJECT_UNSAVED_DRAFT_ARTIFACT = JsonProjector(UnsavedDraftArtifactResult) _PROJECT_SAVED_DRAFT_ARTIFACT = JsonProjector(SavedDraftArtifactResult) +def _prepare_artifact_from_plan( + context: WorkflowOperationContext, + *, + artifact_id: str, + version: int, + title: str, + kind: ArtifactKind, + description: str | None, + plan: RawWorkflowPlan | dict[str, Any], + outcomes: Sequence[str], + required_capabilities: dict[str, dict[str, Any]] | None, + source_bindings: dict[str, str] | None, + created_from_catalog_version: str | None, +) -> WorkflowArtifact: + """Prepare one artifact through the shared plan normalization seam.""" + typed_plan = ( + plan + if isinstance(plan, RawWorkflowPlan) + else RawWorkflowPlan.model_validate(plan) + ) + return build_workflow_artifact_from_plan( + artifact_id=artifact_id, + version=version, + title=title, + kind=kind, + description=description, + plan=typed_plan.model_dump(mode="json", by_alias=True), + outcomes=tuple(outcomes), + required_capabilities={ + name: RequiredCapability.model_validate(capability) + for name, capability in (required_capabilities or {}).items() + }, + source_bindings=source_bindings, + observed_node_specs=observed_node_specs(context), + created_from_catalog_version=created_from_catalog_version, + ) + + +def _invalid_artifact_plan_payload( + diagnostic: ArtifactPlanDiagnosticPayload, +) -> dict[str, Any]: + """Build the complete invalid result, including empty derived inventories.""" + return { + "status": "invalid", + "diagnostics": [diagnostic], + "required_capabilities": [], + "workflow_dependencies": {}, + } + + +def _diagnostic_from_validation_error( + exc: ValidationError, +) -> ArtifactPlanDiagnosticPayload: + """Project the first typed model error with a stable plan-rooted path.""" + error = exc.errors()[0] + location = ".".join(str(part) for part in error["loc"]) + return { + "severity": "error", + "code": "artifact_plan_invalid", + "path": f"plan.{location}" if location else "plan", + "message": str(error["msg"]), + "repair_hint": None, + } + + class WorkflowArtifactApi: """Saved workflow artifact operations. @@ -133,25 +203,17 @@ class WorkflowArtifactApi: source_bindings: dict[str, str] | None = None, created_from_catalog_version: str | None = None, ) -> SaveArtifactResult: - typed_plan = ( - plan - if isinstance(plan, RawWorkflowPlan) - else RawWorkflowPlan.model_validate(plan) - ) - workflow_artifact = build_workflow_artifact_from_plan( + workflow_artifact = _prepare_artifact_from_plan( + self.context, artifact_id=artifact_id, version=version, title=title, kind=kind, description=description, - plan=typed_plan.model_dump(mode="json", by_alias=True), - outcomes=tuple(outcomes), - required_capabilities={ - name: RequiredCapability.model_validate(capability) - for name, capability in (required_capabilities or {}).items() - }, + plan=plan, + outcomes=outcomes, + required_capabilities=required_capabilities, source_bindings=source_bindings, - observed_node_specs=observed_node_specs(self.context), created_from_catalog_version=created_from_catalog_version, ) self._artifact_store().save_artifact(workflow_artifact) @@ -172,6 +234,59 @@ class WorkflowArtifactApi: } ) + async def validate_artifact_plan( + self, + *, + plan: dict[str, Any], + outcomes: Sequence[str], + required_capabilities: dict[str, dict[str, Any]] | None = None, + source_bindings: dict[str, str] | None = None, + ) -> ValidateArtifactPlanResult: + """Validate and inventory a plan without writing the artifact store.""" + try: + # These identity fields satisfy the shared artifact factory only; + # validation never calls the store or emits a saved-artifact event. + artifact = _prepare_artifact_from_plan( + self.context, + artifact_id="__validation__", + version=1, + title="Validation", + kind="workflow", + description=None, + plan=plan, + outcomes=outcomes, + required_capabilities=required_capabilities, + source_bindings=source_bindings, + created_from_catalog_version=None, + ) + except ValidationError as exc: + return _PROJECT_VALIDATE_ARTIFACT( + _invalid_artifact_plan_payload(_diagnostic_from_validation_error(exc)) + ) + except ValueError as exc: + return _PROJECT_VALIDATE_ARTIFACT( + _invalid_artifact_plan_payload( + { + "severity": "error", + "code": "artifact_plan_invalid", + "path": "plan", + "message": str(exc), + "repair_hint": None, + } + ) + ) + return _PROJECT_VALIDATE_ARTIFACT( + { + "status": "valid", + "diagnostics": [], + "required_capabilities": [ + capability.model_dump(mode="json") + for capability in artifact.required_capability_map().values() + ], + "workflow_dependencies": dict(artifact.workflow_dependencies), + } + ) + async def create_artifact_from_draft( self, *, diff --git a/src/wf_api/models/__init__.py b/src/wf_api/models/__init__.py index dd2a9452..a06b3ae5 100644 --- a/src/wf_api/models/__init__.py +++ b/src/wf_api/models/__init__.py @@ -14,12 +14,14 @@ from .admin import ( from .artifacts import ( ArtifactCatalogEntryPayload, ArtifactKindPayload, + ArtifactPlanDiagnosticPayload, CapabilityKindPayload, CapabilityRefPayload, DeleteArtifactResult, ListArtifactsResult, RequiredCapabilityPayload, SaveArtifactResult, + ValidateArtifactPlanResult, WorkflowArtifactPayload, ) from .authoring_contracts import ( @@ -139,6 +141,7 @@ __all__ = [ "AuthoringStepContractPayload", "AuthRecordSummaryPayload", "ArtifactCatalogEntryPayload", + "ArtifactPlanDiagnosticPayload", "ArtifactKindPayload", "CapabilityKindPayload", "CapabilityCallResult", @@ -205,6 +208,7 @@ __all__ = [ "RequiredCapabilityPayload", "RemoveRegistryEntryResult", "SaveArtifactResult", + "ValidateArtifactPlanResult", "SavedDraftArtifactResult", "SaveDeploymentResult", "SourceBindingPayload", diff --git a/src/wf_api/models/artifacts.py b/src/wf_api/models/artifacts.py index b28358ce..19c2fc66 100644 --- a/src/wf_api/models/artifacts.py +++ b/src/wf_api/models/artifacts.py @@ -80,6 +80,25 @@ class SaveArtifactResult(TypedDict): saved: bool +class ArtifactPlanDiagnosticPayload(TypedDict): + """Stable diagnostic projected when an artifact plan is invalid.""" + + severity: Literal["error", "warning"] + code: str + path: str + message: str + repair_hint: str | None + + +class ValidateArtifactPlanResult(TypedDict): + """Non-persisting artifact-plan validation and dependency inventory.""" + + status: Literal["valid", "invalid"] + diagnostics: list[ArtifactPlanDiagnosticPayload] + required_capabilities: list[RequiredCapabilityPayload] + workflow_dependencies: dict[str, int] + + class DeleteArtifactResult(TypedDict): artifact_id: str version: int diff --git a/src/wf_api/service.py b/src/wf_api/service.py index f51716d5..641e9912 100644 --- a/src/wf_api/service.py +++ b/src/wf_api/service.py @@ -46,6 +46,7 @@ from .models import ( SaveArtifactResult, SavedDraftArtifactResult, SaveDeploymentResult, + ValidateArtifactPlanResult, ValidateDeploymentResult, ValidateDraftResult, WorkflowArtifactPayload, @@ -217,6 +218,21 @@ class WorkflowApi: created_from_catalog_version=created_from_catalog_version, ) + async def validate_artifact_plan( + self, + *, + plan: dict[str, Any], + outcomes: Sequence[str], + required_capabilities: dict[str, dict[str, Any]] | None = None, + source_bindings: dict[str, str] | None = None, + ) -> ValidateArtifactPlanResult: + return await self.artifacts.validate_artifact_plan( + plan=plan, + outcomes=outcomes, + required_capabilities=required_capabilities, + source_bindings=source_bindings, + ) + async def create_artifact_from_draft( self, *, diff --git a/src/wf_api/surface.py b/src/wf_api/surface.py index d0331e48..59732c22 100644 --- a/src/wf_api/surface.py +++ b/src/wf_api/surface.py @@ -45,6 +45,7 @@ from .models import ( SaveArtifactResult, SaveDeploymentResult, SourceDiagnosisResult, + ValidateArtifactPlanResult, ValidateDeploymentResult, ValidateDraftResult, WorkflowArtifactPayload, @@ -459,6 +460,15 @@ class WorkflowArtifactSurface(Protocol): created_from_catalog_version: str | None = None, ) -> SaveArtifactResult: ... + async def validate_artifact_plan( + self, + *, + plan: dict[str, Any], + outcomes: Sequence[str], + required_capabilities: dict[str, dict[str, Any]] | None = None, + source_bindings: dict[str, str] | None = None, + ) -> ValidateArtifactPlanResult: ... + class WorkflowDeploymentSurface(Protocol): """Deployment methods exposed by workflow frontends.""" diff --git a/src/wf_transport_rpc_http/client/artifacts.py b/src/wf_transport_rpc_http/client/artifacts.py index e87c1cf6..d5e7796a 100644 --- a/src/wf_transport_rpc_http/client/artifacts.py +++ b/src/wf_transport_rpc_http/client/artifacts.py @@ -7,6 +7,7 @@ from wf_api.models import ( DeleteArtifactResult, ListArtifactsResult, SaveArtifactResult, + ValidateArtifactPlanResult, WorkflowArtifactPayload, ) @@ -67,6 +68,27 @@ class RpcArtifactClientMixin: ), ) + async def validate_artifact_plan( + self: RpcCaller, + *, + plan: dict[str, Any], + outcomes: Sequence[str], + required_capabilities: dict[str, dict[str, Any]] | None = None, + source_bindings: dict[str, str] | None = None, + ) -> ValidateArtifactPlanResult: + return cast( + ValidateArtifactPlanResult, + await self._call( + "workflow.artifacts.validate_plan", + { + "plan": plan, + "outcomes": list(outcomes), + "required_capabilities": required_capabilities, + "source_bindings": source_bindings, + }, + ), + ) + async def create_artifact_from_plan( self: RpcCaller, *, diff --git a/src/wf_transport_rpc_http/methods/artifacts.py b/src/wf_transport_rpc_http/methods/artifacts.py index 775e6b34..8d63d641 100644 --- a/src/wf_transport_rpc_http/methods/artifacts.py +++ b/src/wf_transport_rpc_http/methods/artifacts.py @@ -10,6 +10,7 @@ from wf_api.models import ( DeleteArtifactResult, ListArtifactsResult, SaveArtifactResult, + ValidateArtifactPlanResult, WorkflowArtifactPayload, ) from wf_server import WorkflowServer @@ -21,6 +22,7 @@ from ..models import ( InspectArtifactParams, ListArtifactsParams, SaveArtifactParams, + ValidateArtifactPlanParams, ) from ..params import RpcParams @@ -63,6 +65,22 @@ def register_methods( except (ValueError, KeyError, LookupError, FileNotFoundError) as exc: raise_workflow_rpc_error(exc) + @entrypoint.method( + name="workflow.artifacts.validate_plan", errors=[WorkflowRpcError] + ) + async def workflow_artifacts_validate_plan( + params: ValidateArtifactPlanParams = RpcParams(), + ) -> ValidateArtifactPlanResult: + try: + return await server.api.validate_artifact_plan( + plan=params.plan, + outcomes=tuple(params.outcomes), + required_capabilities=params.required_capabilities, + source_bindings=params.source_bindings, + ) + except (ValueError, KeyError, LookupError, FileNotFoundError) as exc: + raise_workflow_rpc_error(exc) + @entrypoint.method(name="workflow.artifacts.list", errors=[WorkflowRpcError]) async def workflow_artifacts_list( params: ListArtifactsParams = RpcParams(), diff --git a/src/wf_transport_rpc_http/models.py b/src/wf_transport_rpc_http/models.py index faf189d4..0f9479f9 100644 --- a/src/wf_transport_rpc_http/models.py +++ b/src/wf_transport_rpc_http/models.py @@ -120,6 +120,13 @@ class SaveArtifactParams(RpcParamsModel): artifact: dict[str, Any] +class ValidateArtifactPlanParams(RpcParamsModel): + plan: dict[str, Any] + outcomes: list[str] + required_capabilities: dict[str, dict[str, Any]] | None = None + source_bindings: dict[str, str] | None = None + + class SaveDeploymentParams(RpcParamsModel): deployment: dict[str, Any] diff --git a/tests/wf_api/test_artifact_api.py b/tests/wf_api/test_artifact_api.py index d081d89d..5ab4108c 100644 --- a/tests/wf_api/test_artifact_api.py +++ b/tests/wf_api/test_artifact_api.py @@ -185,6 +185,57 @@ async def test_create_artifact_from_plan_saves_with_observed_node_specs( assert saved.id == "echo" +@pytest.mark.asyncio +async def test_validate_artifact_plan_does_not_persist(tmp_path: Path) -> None: + artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_validate") + api, _service = _artifact_api(artifact_store) + + result = await api.validate_artifact_plan( + plan=_echo_artifact().plan, + outcomes=("completed",), + source_bindings={}, + ) + + assert result["status"] == "valid" + assert result["diagnostics"] == [] + assert await api.list_artifacts(query="echo") == { + "nodes": [], + "next_cursor": None, + "total": 0, + } + + +@pytest.mark.asyncio +async def test_validate_artifact_plan_projects_invalid_plan_diagnostic( + tmp_path: Path, +) -> None: + artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_invalid") + api, _service = _artifact_api(artifact_store) + invalid_plan = {**_echo_artifact().plan, "start": "missing"} + + result = await api.validate_artifact_plan( + plan=invalid_plan, + outcomes=("completed",), + source_bindings={}, + ) + + assert result["status"] == "invalid" + assert result["diagnostics"] == [ + { + "severity": "error", + "code": "artifact_plan_invalid", + "path": "plan", + "message": "invalid workflow plan: start node 'missing' does not exist", + "repair_hint": None, + } + ] + assert await api.list_artifacts(query="echo") == { + "nodes": [], + "next_cursor": None, + "total": 0, + } + + @pytest.mark.asyncio async def test_create_artifact_from_workspace_suggests_exact_available_source_binding( tmp_path: Path, diff --git a/tests/wf_contract_manifest/test_generate.py b/tests/wf_contract_manifest/test_generate.py index 5e0378f3..8463cb89 100644 --- a/tests/wf_contract_manifest/test_generate.py +++ b/tests/wf_contract_manifest/test_generate.py @@ -90,9 +90,9 @@ def test_generates_the_complete_real_workflow_contract() -> None: manifest = generate_manifest() schemas = manifest["components"]["schemas"] - assert len(manifest["operations"]) == 71 - assert len({operation["method"] for operation in manifest["operations"]}) == 71 - assert len(schemas) == 140 + assert len(manifest["operations"]) == 72 + assert len({operation["method"] for operation in manifest["operations"]}) == 72 + assert len(schemas) == 142 assert len(manifest["components"]["errors"]) == 1 assert all( set(operation["result"]["schema"]) == {"$ref"} diff --git a/tests/wf_transport_rpc_http/test_app.py b/tests/wf_transport_rpc_http/test_app.py index 9bc9a15d..0a650a47 100644 --- a/tests/wf_transport_rpc_http/test_app.py +++ b/tests/wf_transport_rpc_http/test_app.py @@ -1333,6 +1333,33 @@ async def test_rpc_create_artifact_from_plan(tmp_path) -> None: assert inspected["result"]["plan"]["name"] == "rpc_constant" +async def test_rpc_validate_artifact_plan_does_not_persist(tmp_path) -> None: + server = build_local_static_workflow_server(tmp_path / "store") + app = create_rpc_app(server) + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as client: + validated = await _rpc( + client, + "workflow.artifacts.validate_plan", + { + "plan": _constant_plan().model_dump(mode="json", by_alias=True), + "outcomes": ["ok"], + "source_bindings": {}, + }, + ) + listed = await _rpc( + client, "workflow.artifacts.list", {"query": "rpc_constant"} + ) + + assert validated["result"]["status"] == "valid" + assert validated["result"]["diagnostics"] == [] + assert listed["result"] == { + "nodes": [], + "next_cursor": None, + "total": 0, + } + + async def test_rpc_draft_workspace_focused_edit_methods(tmp_path) -> None: server = build_local_static_workflow_server(tmp_path / "store") app = create_rpc_app(server) diff --git a/tests/wf_transport_rpc_http/test_client.py b/tests/wf_transport_rpc_http/test_client.py index 65b8fb31..acfbdbf8 100644 --- a/tests/wf_transport_rpc_http/test_client.py +++ b/tests/wf_transport_rpc_http/test_client.py @@ -695,6 +695,31 @@ async def test_rpc_client_creates_artifact_from_plan(tmp_path) -> None: assert inspected["id"] == "client_plan" +async def test_rpc_client_validates_artifact_plan_without_persisting(tmp_path) -> None: + server = build_local_static_workflow_server(tmp_path / "store") + app = create_rpc_app(server) + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient( + transport=transport, base_url="http://test" + ) as http_client: + client = RpcWorkflowApiClient( + url="http://test/rpc", + timeout_seconds=5, + http_client=http_client, + ) + validated = await client.validate_artifact_plan( + plan=_constant_plan().model_dump(mode="json", by_alias=True), + outcomes=("ok",), + source_bindings={}, + ) + listed = await client.list_artifacts(query="client_constant") + + assert validated["status"] == "valid" + assert validated["diagnostics"] == [] + assert listed["nodes"] == [] + assert listed["total"] == 0 + + async def test_rpc_client_set_workflow_output_map(tmp_path) -> None: server = build_local_static_workflow_server(tmp_path / "store") app = create_rpc_app(server) diff --git a/tests/wf_transport_rpc_http/test_openrpc_contract.py b/tests/wf_transport_rpc_http/test_openrpc_contract.py index 30680651..4b9669b2 100644 --- a/tests/wf_transport_rpc_http/test_openrpc_contract.py +++ b/tests/wf_transport_rpc_http/test_openrpc_contract.py @@ -399,6 +399,11 @@ def test_openrpc_exposes_typed_auth_delete_result( "SaveArtifactResult", {"artifact_id", "version", "saved"}, ), + ( + "workflow.artifacts.validate_plan", + "ValidateArtifactPlanResult", + {"status", "diagnostics", "required_capabilities", "workflow_dependencies"}, + ), ( "workflow.artifacts.save", "SaveArtifactResult", diff --git a/web/packages/rpc/scripts/workflow-contract-generator.test.ts b/web/packages/rpc/scripts/workflow-contract-generator.test.ts index 408ed199..3b115b86 100644 --- a/web/packages/rpc/scripts/workflow-contract-generator.test.ts +++ b/web/packages/rpc/scripts/workflow-contract-generator.test.ts @@ -436,6 +436,6 @@ describe("workflow contract generator", () => { const generatedSource = await generateWorkflowContractSource(manifestText); expect(generatedSource).toBe(checkedSource); - expect(generatedSource.match(/^ \| "workflow\./gm)).toHaveLength(71); + expect(generatedSource.match(/^ \| "workflow\./gm)).toHaveLength(72); }); }); diff --git a/web/packages/rpc/src/generated/workflow-contract.test.ts b/web/packages/rpc/src/generated/workflow-contract.test.ts index a8bf2298..9e54b809 100644 --- a/web/packages/rpc/src/generated/workflow-contract.test.ts +++ b/web/packages/rpc/src/generated/workflow-contract.test.ts @@ -15,8 +15,8 @@ import { describe("generated workflow contract", () => { it("contains every operation exactly once", () => { - expect(workflowOperationNames).toHaveLength(71); - expect(new Set(workflowOperationNames)).toHaveLength(71); + expect(workflowOperationNames).toHaveLength(72); + expect(new Set(workflowOperationNames)).toHaveLength(72); }); it("contains every authored Effect operation without broadening its boundary", () => { diff --git a/web/packages/rpc/src/generated/workflow-contract.ts b/web/packages/rpc/src/generated/workflow-contract.ts index 744748e3..058be46d 100644 --- a/web/packages/rpc/src/generated/workflow-contract.ts +++ b/web/packages/rpc/src/generated/workflow-contract.ts @@ -22,6 +22,7 @@ export type WorkflowOperationName = | "workflow.artifacts.inspect" | "workflow.artifacts.list" | "workflow.artifacts.save" + | "workflow.artifacts.validate_plan" | "workflow.capabilities.call" | "workflow.capabilities.inspect" | "workflow.capabilities.list" @@ -95,6 +96,7 @@ export const workflowOperationNames: readonly WorkflowOperationName[] = [ "workflow.artifacts.inspect", "workflow.artifacts.list", "workflow.artifacts.save", + "workflow.artifacts.validate_plan", "workflow.capabilities.call", "workflow.capabilities.inspect", "workflow.capabilities.list", @@ -380,6 +382,23 @@ export interface WorkflowContractMap { }; result: SaveArtifactResult; }; + "workflow.artifacts.validate_plan": { + params: { + plan: { + [k: string]: unknown; + }; + outcomes: string[]; + required_capabilities?: { + [k: string]: { + [k: string]: unknown; + }; + } | null; + source_bindings?: { + [k: string]: string; + } | null; + }; + result: ValidateArtifactPlanResult; + }; "workflow.capabilities.call": { params: { qualified_name: string; @@ -1248,6 +1267,35 @@ export interface ArtifactCatalogEntryPayload { version: number; [k: string]: unknown; } +/** + * Non-persisting artifact-plan validation and dependency inventory. + * + * This interface was referenced by `WorkflowContractMap`'s JSON-Schema + * via the `definition` "ValidateArtifactPlanResult". + */ +export interface ValidateArtifactPlanResult { + diagnostics: ArtifactPlanDiagnosticPayload[]; + required_capabilities: RequiredCapabilityPayload[]; + status: "valid" | "invalid"; + workflow_dependencies: { + [k: string]: number; + }; + [k: string]: unknown; +} +/** + * Stable diagnostic projected when an artifact plan is invalid. + * + * This interface was referenced by `WorkflowContractMap`'s JSON-Schema + * via the `definition` "ArtifactPlanDiagnosticPayload". + */ +export interface ArtifactPlanDiagnosticPayload { + code: string; + message: string; + path: string; + repair_hint: string | null; + severity: "error" | "warning"; + [k: string]: unknown; +} /** * Outcome returned by a direct node-spec or wrapper capability call. *