diff --git a/.agents/skills/jsonschema-to-pydantic-lungo/SKILL.md b/.agents/skills/jsonschema-to-pydantic-lungo/SKILL.md index 13dba793d..d66bbef3c 100644 --- a/.agents/skills/jsonschema-to-pydantic-lungo/SKILL.md +++ b/.agents/skills/jsonschema-to-pydantic-lungo/SKILL.md @@ -271,7 +271,7 @@ If a consumer fails because of a rename you introduced (e.g. it imported the old - ❌ Using `@model_validator(mode="after")` to police `__pydantic_extra__` for fields that should belong to a sibling union variant. - ❌ Putting field-count or per-field validity checks inside a `Discriminator` callable. The discriminator answers exactly one question: which schema `anyOf` branch. - ❌ Adding `@field_validator` for a constraint that fits in `Annotated[..., Field(...)]`. -- ❌ Inventing class names (`AgentId` for `stable_agent_id`, `Node` for `regular_node`). Names come straight from `$defs` keys. +- ❌ Inventing class names (`AgentId` for `stable_agent_id`, `Node` for `base_node`). Names come straight from `$defs` keys. - ❌ Hand-editing a small portion of the generated file in response to a test failure or a request for a tweak. Re-emit the whole file end-to-end via the skill instead — partial patches drift from the rules over time. ## Reference diff --git a/.agents/skills/jsonschema-to-pydantic-lungo/mapping-rules.md b/.agents/skills/jsonschema-to-pydantic-lungo/mapping-rules.md index dd83a52e7..ab4cdbb5a 100644 --- a/.agents/skills/jsonschema-to-pydantic-lungo/mapping-rules.md +++ b/.agents/skills/jsonschema-to-pydantic-lungo/mapping-rules.md @@ -57,7 +57,7 @@ Do not emit a separate `@field_validator` that loops the keys checking the patte Schema: ```json -"partial_regular_node": { +"partial_base_node": { "type": "object", "required": ["id", "operation"], "properties": { @@ -70,9 +70,9 @@ Schema: }, "additionalProperties": true }, -"regular_node": { +"base_node": { "allOf": [ - { "$ref": "#/$defs/partial_regular_node" }, + { "$ref": "#/$defs/partial_base_node" }, { "type": "object", "required": ["id", "operation", "type", "label", "size", "layer_index"] @@ -84,7 +84,7 @@ Schema: Emit two unrelated classes (no inheritance): ```python -class PartialRegularNode(BaseModel): +class PartialBaseNode(BaseModel): model_config = ConfigDict(extra="allow") id: NodeId @@ -95,7 +95,7 @@ class PartialRegularNode(BaseModel): layer_index: float = 0 -class RegularNode(BaseModel): # NOT (PartialRegularNode) +class BaseNode(BaseModel): # NOT (PartialBaseNode) model_config = ConfigDict(extra="allow") id: NodeId @@ -106,7 +106,7 @@ class RegularNode(BaseModel): # NOT (PartialRegularNode) layer_index: float ``` -Why no subclassing: `type: ... | None = None` from `PartialRegularNode` would leak into `RegularNode` and `RegularNode(id=..., operation=...)` would silently validate. +Why no subclassing: `type: ... | None = None` from `PartialBaseNode` would leak into `BaseNode` and `BaseNode(id=..., operation=...)` would silently validate. ## §D — `anyOf` discriminated by sibling-key presence @@ -129,13 +129,13 @@ Schema (the canonical lungo case for `partial_node` / `node`). The agent-flavour }, "partial_agent_node": { "allOf": [ - { "$ref": "#/$defs/partial_regular_node" }, + { "$ref": "#/$defs/partial_base_node" }, { "$ref": "#/$defs/partial_node_agent_extension" } ] }, "agent_node": { "allOf": [ - { "$ref": "#/$defs/regular_node" }, + { "$ref": "#/$defs/base_node" }, { "$ref": "#/$defs/node_agent_extension" } ] }, @@ -143,7 +143,7 @@ Schema (the canonical lungo case for `partial_node` / `node`). The agent-flavour "anyOf": [ { "allOf": [ - { "$ref": "#/$defs/partial_regular_node" }, + { "$ref": "#/$defs/partial_base_node" }, { "not": { "anyOf": [ { "required": ["agent_record_uri"] }, { "required": ["stable_agent_id"] } @@ -153,44 +153,44 @@ Schema (the canonical lungo case for `partial_node` / `node`). The agent-flavour { "$ref": "#/$defs/partial_agent_node" } ] }, -"node": { /* same shape: regular branch + $ref agent_node */ }, +"node": { /* same shape: base branch + $ref agent_node */ }, "topology_node_item": { "anyOf": [{ "$ref": "#/$defs/partial_node" }, { "$ref": "#/$defs/node" }] } ``` -Step 1 — emit the four leaf classes. Names come straight from the `$defs` keys (no merged-class invention — see [`SKILL.md`](SKILL.md)). The agent variants subclass their regular counterparts because the extension is purely *additive* fields (no required-vs-optional drift, unlike §C): +Step 1 — emit the four leaf classes. Names come straight from the `$defs` keys (no merged-class invention — see [`SKILL.md`](SKILL.md)). The agent variants subclass their base counterparts because the extension is purely *additive* fields (no required-vs-optional drift, unlike §C): ```python -class PartialAgentNode(PartialRegularNode): +class PartialAgentNode(PartialBaseNode): agent_record_uri: Annotated[str, Field(min_length=1)] stable_agent_id: StableAgentId | None = None -class AgentNode(RegularNode): +class AgentNode(BaseNode): agent_record_uri: Annotated[str, Field(min_length=1)] stable_agent_id: StableAgentId ``` `partial_node_agent_extension` and `node_agent_extension` are **not** emitted as standalone classes: they are only ever referenced inside the `allOf`s that build `partial_agent_node` / `agent_node`, so their fields appear directly on the agent classes. -Step 2 — discriminator that mirrors **only** the sibling-key presence test that the regular branch's `not { anyOf: [...required...] }` clause is making: +Step 2 — discriminator that mirrors **only** the sibling-key presence test that the base branch's `not { anyOf: [...required...] }` clause is making: ```python -_REGULAR_TAG = "regular" +_BASE_TAG = "base" _AGENT_TAG = "agent" def _node_kind_discriminator(value: Any) -> str | None: if isinstance(value, (AgentNode, PartialAgentNode)): return _AGENT_TAG - if isinstance(value, (RegularNode, PartialRegularNode)): - return _REGULAR_TAG + if isinstance(value, (BaseNode, PartialBaseNode)): + return _BASE_TAG if not isinstance(value, dict): return None if "agent_record_uri" in value or "stable_agent_id" in value: return _AGENT_TAG - return _REGULAR_TAG + return _BASE_TAG ``` Step 3 — emit each `anyOf`-shaped `$def` as an `Annotated[Union[...], Discriminator(...)]`. Inside each tagged branch use a plain `Union[Full, Partial]` and let smart-union pick the right one based on field population: @@ -198,7 +198,7 @@ Step 3 — emit each `anyOf`-shaped `$def` as an `Annotated[Union[...], Discrimi ```python TopologyNodeItem = Annotated[ Union[ - Annotated[Union[RegularNode, PartialRegularNode], Tag(_REGULAR_TAG)], + Annotated[Union[BaseNode, PartialBaseNode], Tag(_BASE_TAG)], Annotated[Union[AgentNode, PartialAgentNode], Tag(_AGENT_TAG)], ], Discriminator(_node_kind_discriminator), @@ -206,7 +206,7 @@ TopologyNodeItem = Annotated[ PartialNode = Annotated[ Union[ - Annotated[PartialRegularNode, Tag(_REGULAR_TAG)], + Annotated[PartialBaseNode, Tag(_BASE_TAG)], Annotated[PartialAgentNode, Tag(_AGENT_TAG)], ], Discriminator(_node_kind_discriminator), @@ -214,7 +214,7 @@ PartialNode = Annotated[ Node = Annotated[ Union[ - Annotated[RegularNode, Tag(_REGULAR_TAG)], + Annotated[BaseNode, Tag(_BASE_TAG)], Annotated[AgentNode, Tag(_AGENT_TAG)], ], Discriminator(_node_kind_discriminator), @@ -225,8 +225,8 @@ Node = Annotated[ | Input | Routes to | Outcome | |---|---|---| -| All regular fields, no extension keys | `RegularNode` | OK | -| Only `id`+`operation` | `PartialRegularNode` | OK | +| All base fields, no extension keys | `BaseNode` | OK | +| Only `id`+`operation` | `PartialBaseNode` | OK | | All fields + `agent_record_uri` + valid `stable_agent_id` | `AgentNode` | OK | | `agent_record_uri` only (partial) | `PartialAgentNode` | OK | | `stable_agent_id` only, no `agent_record_uri` | agent branch → `PartialAgentNode` | rejected: `agent_record_uri` is required | @@ -236,10 +236,10 @@ The error messages come straight from each branch's own `Field` constraints — ### Anti-pattern this replaces -The earlier (incorrect) approach was an `@model_validator(mode="after")` on `PartialRegularNode` / `RegularNode` that walked `__pydantic_extra__` and raised if `agent_record_uri` or `stable_agent_id` had leaked through `extra="allow"`. That approach: +The earlier (incorrect) approach was an `@model_validator(mode="after")` on `PartialBaseNode` / `BaseNode` that walked `__pydantic_extra__` and raised if `agent_record_uri` or `stable_agent_id` had leaked through `extra="allow"`. That approach: - Required a parallel `_AGENT_EXTENSION_FIELDS` allow-list. - Hid the schema's `pattern` / `min_length` errors behind a generic message. -- Coupled `RegularNode` to knowledge of its sibling extension class. +- Coupled `BaseNode` to knowledge of its sibling extension class. The discriminator solves all three problems at once: routing alone is sufficient because each variant's own field constraints handle the rest. diff --git a/coffeeAGNTCY/coffee_agents/lungo/common/workflow_utils/builders.py b/coffeeAGNTCY/coffee_agents/lungo/common/workflow_utils/builders.py index 4775f165e..29c05e19a 100644 --- a/coffeeAGNTCY/coffee_agents/lungo/common/workflow_utils/builders.py +++ b/coffeeAGNTCY/coffee_agents/lungo/common/workflow_utils/builders.py @@ -23,7 +23,7 @@ Operation, PartialEdge, PartialNode, - PartialRegularNode, + PartialBaseNode, PartialTopology, Size, Topology, @@ -67,7 +67,7 @@ def make_node( node_extras["agent_record_uri"] = f"agent-card://{stable_agent_id.removeprefix('agent://')}" if extras: node_extras.update(extras) - return PartialRegularNode( + return PartialBaseNode( id=NodeId(node_id), operation=operation, type=node_type, diff --git a/coffeeAGNTCY/coffee_agents/lungo/schema/jsonschemas/event_v1.json b/coffeeAGNTCY/coffee_agents/lungo/schema/jsonschemas/event_v1.json index 61ee0fba5..2bfdd7c16 100644 --- a/coffeeAGNTCY/coffee_agents/lungo/schema/jsonschemas/event_v1.json +++ b/coffeeAGNTCY/coffee_agents/lungo/schema/jsonschemas/event_v1.json @@ -55,9 +55,9 @@ }, "additionalProperties": false }, - "partial_regular_node": { + "partial_base_node": { "type": "object", - "description": "Sparse regular node data (mainly for updates). Used in topology definitions.", + "description": "Sparse base node data (mainly for updates). Used in topology definitions.", "required": ["id", "operation"], "properties": { "id": { "$ref": "#/$defs/node_id" }, @@ -69,12 +69,12 @@ }, "additionalProperties": true }, - "regular_node": { + "base_node": { "allOf": [ - { "$ref": "#/$defs/partial_regular_node" }, + { "$ref": "#/$defs/partial_base_node" }, { "type": "object", - "description": "Full regular node data (mainly for init/reset). Used in topology definitions.", + "description": "Full base node data (mainly for init/reset). Used in topology definitions.", "required": ["id", "operation", "type", "label", "size", "layer_index"] } ] @@ -99,25 +99,25 @@ ] }, "partial_agent_node": { - "description": "Sparse agent node data (mainly for updates): a partial_regular_node combined with a partial_node_agent_extension.", + "description": "Sparse agent node data (mainly for updates): a partial_base_node combined with a partial_node_agent_extension.", "allOf": [ - { "$ref": "#/$defs/partial_regular_node" }, + { "$ref": "#/$defs/partial_base_node" }, { "$ref": "#/$defs/partial_node_agent_extension" } ] }, "agent_node": { - "description": "Full agent node data (mainly for init/reset): a regular_node combined with a node_agent_extension.", + "description": "Full agent node data (mainly for init/reset): a base_node combined with a node_agent_extension.", "allOf": [ - { "$ref": "#/$defs/regular_node" }, + { "$ref": "#/$defs/base_node" }, { "$ref": "#/$defs/node_agent_extension" } ] }, "partial_node": { - "description": "Sparse node data: either a partial_regular_node alone (without any agent extension fields), or a partial_agent_node.", + "description": "Sparse node data: either a partial_base_node alone (without any agent extension fields), or a partial_agent_node.", "anyOf": [ { "allOf": [ - { "$ref": "#/$defs/partial_regular_node" }, + { "$ref": "#/$defs/partial_base_node" }, { "not": { "anyOf": [ @@ -132,11 +132,11 @@ ] }, "node": { - "description": "Full node data: either a regular_node alone (without any agent extension fields), or an agent_node.", + "description": "Full node data: either a base_node alone (without any agent extension fields), or an agent_node.", "anyOf": [ { "allOf": [ - { "$ref": "#/$defs/regular_node" }, + { "$ref": "#/$defs/base_node" }, { "not": { "anyOf": [ diff --git a/coffeeAGNTCY/coffee_agents/lungo/schema/types/__init__.py b/coffeeAGNTCY/coffee_agents/lungo/schema/types/__init__.py index 2c425a0f6..7a33a91f7 100644 --- a/coffeeAGNTCY/coffee_agents/lungo/schema/types/__init__.py +++ b/coffeeAGNTCY/coffee_agents/lungo/schema/types/__init__.py @@ -15,6 +15,7 @@ from schema.types.event import ( AgentNode, + BaseNode, Correlation, CorrelationId, Data, @@ -28,11 +29,10 @@ NodeId, Operation, PartialAgentNode, + PartialBaseNode, PartialEdge, PartialNode, - PartialRegularNode, PartialTopology, - RegularNode, Size, StableAgentId, Topology, @@ -51,6 +51,7 @@ __all__ = [ "AgentNode", + "BaseNode", "Correlation", "CorrelationId", "Data", @@ -65,11 +66,10 @@ "NodeId", "Operation", "PartialAgentNode", + "PartialBaseNode", "PartialEdge", "PartialNode", - "PartialRegularNode", "PartialTopology", - "RegularNode", "Size", "StableAgentId", "Topology", diff --git a/coffeeAGNTCY/coffee_agents/lungo/schema/types/event.py b/coffeeAGNTCY/coffee_agents/lungo/schema/types/event.py index c7bd86a27..28f24181a 100644 --- a/coffeeAGNTCY/coffee_agents/lungo/schema/types/event.py +++ b/coffeeAGNTCY/coffee_agents/lungo/schema/types/event.py @@ -12,18 +12,18 @@ The class hierarchy mirrors the ``$defs`` of the source schema: -* ``RegularNode`` / ``Edge`` / ``Topology`` are **not** subclasses of their +* ``BaseNode`` / ``Edge`` / ``Topology`` are **not** subclasses of their partial counterparts: their fields are repeated as required so JSON Schema ``allOf`` + ``required`` semantics are preserved without inheriting optional ``| None`` fields (Rule C). * ``PartialAgentNode`` / ``AgentNode`` correspond to ``$defs.partial_agent_node`` - / ``$defs.agent_node``. They subclass the regular node classes because the + / ``$defs.agent_node``. They subclass the base node classes because the ``partial_node_agent_extension`` / ``node_agent_extension`` composition only *adds* fields (Rule D step 1). * ``$defs.partial_node`` / ``$defs.node`` are sibling-key-discriminated ``anyOf`` unions (Rule D): a callable Pydantic ``Discriminator`` mirrors the schema's ``not { required: [...] }`` test and routes inputs to the - ``regular`` or ``agent`` branch; smart-union picks ``Full`` over ``Partial`` + ``base`` or ``agent`` branch; smart-union picks ``Full`` over ``Partial`` inside each branch. * Cross-field constraints not expressible in JSON Schema (e.g. ``workflow.instances`` map keys must equal the nested ``id``) are encoded as @@ -209,7 +209,7 @@ class Size(BaseModel): # --------------------------------------------------------------------------- -# Node hierarchy (``$defs.partial_regular_node`` / ``regular_node`` / +# Node hierarchy (``$defs.partial_base_node`` / ``base_node`` / # ``partial_agent_node`` / ``agent_node`` / ``partial_node`` / ``node``). # # ``$defs.partial_node_agent_extension`` and ``$defs.node_agent_extension`` @@ -220,8 +220,8 @@ class Size(BaseModel): # --------------------------------------------------------------------------- -class PartialRegularNode(BaseModel): - """Generated from ``$defs.partial_regular_node``: sparse regular node data +class PartialBaseNode(BaseModel): + """Generated from ``$defs.partial_base_node``: sparse base node data (mainly for updates). ``additionalProperties: true``; agent-extension keys live on the ``PartialAgentNode`` subclass and are routed there by the ``$defs.partial_node`` / ``$defs.node`` discriminator.""" @@ -236,10 +236,10 @@ class PartialRegularNode(BaseModel): layer_index: float = 0 -class RegularNode(BaseModel): - """Generated from ``$defs.regular_node``: full regular node data (mainly +class BaseNode(BaseModel): + """Generated from ``$defs.base_node``: full base node data (mainly for init/reset). All listed fields are required. Standalone class (not a - subclass of ``PartialRegularNode``) so optional ``| None`` field types do + subclass of ``PartialBaseNode``) so optional ``| None`` field types do not leak into the required form.""" model_config = ConfigDict(extra="allow") @@ -252,8 +252,8 @@ class RegularNode(BaseModel): layer_index: float -class PartialAgentNode(PartialRegularNode): - """Generated from ``$defs.partial_agent_node``: ``partial_regular_node`` +class PartialAgentNode(PartialBaseNode): + """Generated from ``$defs.partial_agent_node``: ``partial_base_node`` combined with ``partial_node_agent_extension``. ``agent_record_uri`` is required by the extension; ``stable_agent_id`` remains optional.""" @@ -261,8 +261,8 @@ class PartialAgentNode(PartialRegularNode): stable_agent_id: StableAgentId | None = None -class AgentNode(RegularNode): - """Generated from ``$defs.agent_node``: ``regular_node`` combined with +class AgentNode(BaseNode): + """Generated from ``$defs.agent_node``: ``base_node`` combined with ``node_agent_extension``. Both ``agent_record_uri`` and ``stable_agent_id`` are required.""" @@ -271,32 +271,32 @@ class AgentNode(RegularNode): # ``$defs.partial_node`` and ``$defs.node`` are ``anyOf`` choices between a -# *regular* shape and an *agent_node* shape, distinguished by the presence of +# *base* shape and an *agent_node* shape, distinguished by the presence of # ``agent_record_uri`` / ``stable_agent_id``. We mirror exactly that single # decision with a callable Pydantic discriminator and let smart union handle # the *full vs. partial* sub-choice (which is a pure more-required-fields # question) inside each branch. -_REGULAR_TAG = "regular" +_BASE_TAG = "base" _AGENT_TAG = "agent" def _node_kind_discriminator(value: Any) -> str | None: - """Route to the *regular* or *agent* node branch. + """Route to the *base* or *agent* node branch. Returns ``"agent"`` when the input carries any ``$defs.partial_node_agent_extension`` key (``agent_record_uri`` or - ``stable_agent_id``); otherwise ``"regular"``. + ``stable_agent_id``); otherwise ``"base"``. """ if isinstance(value, (AgentNode, PartialAgentNode)): return _AGENT_TAG - if isinstance(value, (RegularNode, PartialRegularNode)): - return _REGULAR_TAG + if isinstance(value, (BaseNode, PartialBaseNode)): + return _BASE_TAG if not isinstance(value, dict): return None if "agent_record_uri" in value or "stable_agent_id" in value: return _AGENT_TAG - return _REGULAR_TAG + return _BASE_TAG # Generated from ``$defs.topology_node_item`` (anyOf of partial_node and node). @@ -304,7 +304,7 @@ def _node_kind_discriminator(value: Any) -> str | None: # required fields are populated and falls back to the partial variant otherwise. TopologyNodeItem = Annotated[ Union[ - Annotated[Union[RegularNode, PartialRegularNode], Tag(_REGULAR_TAG)], + Annotated[Union[BaseNode, PartialBaseNode], Tag(_BASE_TAG)], Annotated[Union[AgentNode, PartialAgentNode], Tag(_AGENT_TAG)], ], Discriminator(_node_kind_discriminator), @@ -313,7 +313,7 @@ def _node_kind_discriminator(value: Any) -> str | None: # Generated from ``$defs.partial_node`` (anyOf). PartialNode = Annotated[ Union[ - Annotated[PartialRegularNode, Tag(_REGULAR_TAG)], + Annotated[PartialBaseNode, Tag(_BASE_TAG)], Annotated[PartialAgentNode, Tag(_AGENT_TAG)], ], Discriminator(_node_kind_discriminator), @@ -322,7 +322,7 @@ def _node_kind_discriminator(value: Any) -> str | None: # Generated from ``$defs.node`` (anyOf). Node = Annotated[ Union[ - Annotated[RegularNode, Tag(_REGULAR_TAG)], + Annotated[BaseNode, Tag(_BASE_TAG)], Annotated[AgentNode, Tag(_AGENT_TAG)], ], Discriminator(_node_kind_discriminator), diff --git a/coffeeAGNTCY/coffee_agents/lungo/tests/unit/agentic_workflows/test_workflow_topology_edge_remap.py b/coffeeAGNTCY/coffee_agents/lungo/tests/unit/agentic_workflows/test_workflow_topology_edge_remap.py index 74b4c1078..9640f1903 100644 --- a/coffeeAGNTCY/coffee_agents/lungo/tests/unit/agentic_workflows/test_workflow_topology_edge_remap.py +++ b/coffeeAGNTCY/coffee_agents/lungo/tests/unit/agentic_workflows/test_workflow_topology_edge_remap.py @@ -11,7 +11,7 @@ _remap_starting_topology_edge_endpoints, _require_old_id_to_unique_label, ) -from schema.types import Edge, NodeId, Operation, PartialEdge, PartialRegularNode, RegularNode +from schema.types import Edge, NodeId, Operation, PartialEdge, PartialBaseNode, BaseNode def _edge( @@ -32,8 +32,8 @@ def _edge( ) -def _regular_node(nid: str, label: str) -> RegularNode: - return RegularNode.model_validate( +def _base_node(nid: str, label: str) -> BaseNode: + return BaseNode.model_validate( { "id": nid, "operation": Operation.READ.value, @@ -111,7 +111,7 @@ def test_remap_unknown_edge_endpoint_raises() -> None: def test_require_unique_label_missing_raises() -> None: - n = PartialRegularNode.model_validate( + n = PartialBaseNode.model_validate( { "id": "node://aaaaaaaa-0000-4000-a000-000000000099", "operation": Operation.READ.value, @@ -124,7 +124,7 @@ def test_require_unique_label_missing_raises() -> None: def test_require_unique_label_duplicate_raises() -> None: - a = _regular_node("node://aaaaaaaa-0000-4000-a000-000000000001", "Same") - b = _regular_node("node://bbbbbbbb-0000-4000-a000-000000000002", "Same") + a = _base_node("node://aaaaaaaa-0000-4000-a000-000000000001", "Same") + b = _base_node("node://bbbbbbbb-0000-4000-a000-000000000002", "Same") with pytest.raises(ValueError, match="duplicate node label"): _require_old_id_to_unique_label([a, b], workflow_name="DupWf", idx_wf=0) diff --git a/coffeeAGNTCY/coffee_agents/lungo/tests/unit/schemas/test_schema_types_against_validation.py b/coffeeAGNTCY/coffee_agents/lungo/tests/unit/schemas/test_schema_types_against_validation.py index 587e35f01..7eed3ea0d 100644 --- a/coffeeAGNTCY/coffee_agents/lungo/tests/unit/schemas/test_schema_types_against_validation.py +++ b/coffeeAGNTCY/coffee_agents/lungo/tests/unit/schemas/test_schema_types_against_validation.py @@ -5,6 +5,7 @@ from __future__ import annotations +import json from copy import deepcopy from pathlib import Path from typing import Callable, NamedTuple @@ -19,6 +20,7 @@ KNOWN = "event_v1" _LUNGO_ROOT = Path(__file__).resolve().parents[3] _EXAMPLES = _LUNGO_ROOT / "schema" / "jsonschemas" / "examples" +_EVENT_SCHEMA = _LUNGO_ROOT / "schema" / "jsonschemas" / "event_v1.json" _INSTANCE_KEY = "instance://550e8400-e29b-41d4-a716-446655440003" @@ -142,3 +144,20 @@ def test_event_payload_schema_and_model(case: Case) -> None: assert isinstance(dumped["metadata"]["timestamp"], str) assert event.metadata.timestamp.tzinfo is not None + + +def test_node_schema_and_public_types_use_base_names() -> None: + schema = json.loads(_EVENT_SCHEMA.read_text()) + defs = schema["$defs"] + + assert "partial_base_node" in defs + assert "base_node" in defs + assert "partial_regular_node" not in defs + assert "regular_node" not in defs + + import schema.types as schema_types + + assert hasattr(schema_types, "PartialBaseNode") + assert hasattr(schema_types, "BaseNode") + assert not hasattr(schema_types, "PartialRegularNode") + assert not hasattr(schema_types, "RegularNode")