workflow capabilityref: more models

This commit is contained in:
lda
2026-05-18 02:39:02 +07:00 Verified
parent 90e2141be3
commit b288671aee
6 changed files with 84 additions and 10 deletions
+5
View File
@@ -577,6 +577,11 @@ Dot-joined names remain the wire/presentation format, but new runtime code
should parse or format through those refs instead of rediscovering source/name should parse or format through those refs instead of rediscovering source/name
boundaries with ad hoc string splits. boundaries with ad hoc string splits.
Saved workflow artifact names use a separate grammar and ref type:
`WorkflowCapabilityRef(artifact_id, version)` serializes as
`workflow.<artifact_id>.v<version>`. Artifact ids may contain dots, so this
must not be parsed as a generic `CapabilityRef`.
The first implementation should prefer artifact validation and dependency The first implementation should prefer artifact validation and dependency
diagnostics before attempting persistent nested resume. diagnostics before attempting persistent nested resume.
+2
View File
@@ -15,6 +15,7 @@ from .models import (
WorkflowArtifact, WorkflowArtifact,
WorkflowDeployment, WorkflowDeployment,
) )
from .refs import WorkflowCapabilityRef
from .store import FileWorkflowArtifactStore, WorkflowArtifactStore from .store import FileWorkflowArtifactStore, WorkflowArtifactStore
from .validation import validate_deployment_dependencies from .validation import validate_deployment_dependencies
from .references import logical_ref_for_concrete_ref, normalize_plan_node_refs from .references import logical_ref_for_concrete_ref, normalize_plan_node_refs
@@ -30,6 +31,7 @@ __all__ = [
"RequiredCapability", "RequiredCapability",
"WorkflowArtifact", "WorkflowArtifact",
"WorkflowArtifactCatalogEntry", "WorkflowArtifactCatalogEntry",
"WorkflowCapabilityRef",
"WorkflowArtifactStore", "WorkflowArtifactStore",
"WorkflowDeployment", "WorkflowDeployment",
"artifact_catalog_entry", "artifact_catalog_entry",
+7 -1
View File
@@ -3,6 +3,7 @@ from __future__ import annotations
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from .models import DependencyDiagnostic, JsonObject, WorkflowArtifact from .models import DependencyDiagnostic, JsonObject, WorkflowArtifact
from .refs import WorkflowCapabilityRef
class WorkflowArtifactCatalogEntry(BaseModel): class WorkflowArtifactCatalogEntry(BaseModel):
@@ -23,7 +24,12 @@ class WorkflowArtifactCatalogEntry(BaseModel):
def artifact_node_name(artifact: WorkflowArtifact) -> str: def artifact_node_name(artifact: WorkflowArtifact) -> str:
"""Return the stable planner name for an artifact version.""" """Return the stable planner name for an artifact version."""
return f"workflow.{artifact.id}.v{artifact.version}" return str(
WorkflowCapabilityRef(
artifact_id=artifact.id,
version=artifact.version,
)
)
def artifact_catalog_entry( def artifact_catalog_entry(
+33
View File
@@ -0,0 +1,33 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class WorkflowCapabilityRef:
"""Stable public capability name for one saved workflow artifact version."""
artifact_id: str
version: int
def __post_init__(self) -> None:
if not self.artifact_id:
raise ValueError("workflow capability ref requires an artifact id")
if self.version < 1:
raise ValueError("workflow capability ref requires version >= 1")
@classmethod
def parse(cls, value: str) -> WorkflowCapabilityRef:
"""Parse `workflow.<artifact_id>.v<version>` into first-class fields."""
prefix = "workflow."
if not value.startswith(prefix):
raise ValueError("workflow capability ref must use the workflow namespace")
artifact_part, separator, version_part = value[len(prefix) :].rpartition(".v")
if not separator or not artifact_part or not version_part.isdecimal():
raise ValueError(
"workflow capability ref must be workflow.<artifact_id>.v<version>"
)
return cls(artifact_id=artifact_part, version=int(version_part))
def __str__(self) -> str:
return f"workflow.{self.artifact_id}.v{self.version}"
+11 -9
View File
@@ -11,6 +11,7 @@ from wf_artifacts import (
DiagnosticSeverity, DiagnosticSeverity,
RequiredCapability, RequiredCapability,
WorkflowArtifact, WorkflowArtifact,
WorkflowCapabilityRef,
WorkflowDeployment, WorkflowDeployment,
create_workflow_artifact_from_plan as build_workflow_artifact_from_plan, create_workflow_artifact_from_plan as build_workflow_artifact_from_plan,
validate_deployment_dependencies, validate_deployment_dependencies,
@@ -356,20 +357,21 @@ def _capability_name(qualified_name: str) -> str | None:
def _artifact_capability_id(artifact: WorkflowArtifact) -> str: def _artifact_capability_id(artifact: WorkflowArtifact) -> str:
"""Use the same stable name shape as workflow artifact catalog entries.""" """Use the same stable name shape as workflow artifact catalog entries."""
return f"workflow.{artifact.id}.v{artifact.version}" return str(
WorkflowCapabilityRef(
artifact_id=artifact.id,
version=artifact.version,
)
)
def _parse_artifact_capability_id(qualified_name: str) -> tuple[str, int] | None: def _parse_artifact_capability_id(qualified_name: str) -> tuple[str, int] | None:
"""Parse the stable `workflow.<artifact_id>.v<version>` capability name.""" """Parse the stable `workflow.<artifact_id>.v<version>` capability name."""
prefix = "workflow." try:
if not qualified_name.startswith(prefix): ref = WorkflowCapabilityRef.parse(qualified_name)
except ValueError:
return None return None
artifact_part, separator, version_part = qualified_name[len(prefix) :].rpartition( return ref.artifact_id, ref.version
".v"
)
if not separator or not artifact_part or not version_part.isdecimal():
return None
return artifact_part, int(version_part)
def _raw_plan_from_artifact(artifact: WorkflowArtifact) -> RawWorkflowPlan: def _raw_plan_from_artifact(artifact: WorkflowArtifact) -> RawWorkflowPlan:
+26
View File
@@ -0,0 +1,26 @@
from __future__ import annotations
from wf_artifacts import WorkflowCapabilityRef
def test_workflow_capability_ref_round_trips() -> None:
ref = WorkflowCapabilityRef(artifact_id="echo_wrapper", version=2)
assert str(ref) == "workflow.echo_wrapper.v2"
assert WorkflowCapabilityRef.parse(str(ref)) == ref
def test_workflow_capability_ref_preserves_dotted_artifact_ids() -> None:
ref = WorkflowCapabilityRef.parse("workflow.crm.lookup.v3")
assert ref.artifact_id == "crm.lookup"
assert ref.version == 3
def test_workflow_capability_ref_rejects_other_namespaces() -> None:
try:
WorkflowCapabilityRef.parse("demo.echo.v1")
except ValueError as exc:
assert "workflow" in str(exc)
else:
raise AssertionError("expected non-workflow ref to be rejected")