fix: advertise structural workflow paths in schemas
This commit is contained in:
@@ -280,9 +280,10 @@ stable.
|
||||
- Completed: challenge matrix operations now have compact OpenCode thread
|
||||
titles, policy handling for canonical skill-document reads, and a central
|
||||
`summarize_trials.py` command for audited result tables.
|
||||
- Completed: canonical TOML path strings replace structural JSON path objects
|
||||
in workflow drafts. Paths now emit as `"input.text"`, `"state.echoed"`,
|
||||
`"message"` (local) instead of `{"root": "input", "parts": ["text"]}`.
|
||||
- Completed: canonical TOML path strings are the emitted workflow path form.
|
||||
Paths now serialize as `"input.text"`, `"state.echoed"`, and `"message"`
|
||||
(local). Structural `{"root": "input", "parts": ["text"]}` path objects
|
||||
remain accepted and are now advertised in generated schemas as an input form.
|
||||
- Completed: challenge-driven CLI UX fixes now provide exact available
|
||||
deployment binding suggestions, reject bare `--bind-output` state targets
|
||||
before RPC with compact guidance, and accept `wf schema --full` as an alias
|
||||
|
||||
@@ -84,8 +84,9 @@ New canonical graph path JSON uses TOML-key strings:
|
||||
"input.message"
|
||||
```
|
||||
|
||||
Structural root/parts objects are still accepted at model-parse boundaries for
|
||||
old persisted records, but new schemas and new examples should emit strings.
|
||||
Structural root/parts objects are still accepted at model-parse boundaries and
|
||||
are advertised by generated schemas as an input form. Serializers and new
|
||||
examples should emit strings.
|
||||
Quote a segment when the field name itself contains a dot or space, for example
|
||||
`state."person.name"` or `state.person."full name"`.
|
||||
|
||||
|
||||
@@ -313,12 +313,11 @@ formatter. The shared grammar must support quoted TOML keys for literal dots,
|
||||
spaces, and other non-bare segments. Parse errors identify the complete input
|
||||
and recommend quoting the invalid segment.
|
||||
|
||||
Pydantic JSON schemas advertise path strings rather than the structural
|
||||
`{root, parts}` object. Serializers emit canonical strings. Validators continue
|
||||
to accept the structural object only as a read-compatibility path for existing
|
||||
persisted drafts, artifacts, and runs; new public examples and writes use
|
||||
strings. This is compatibility for real stored data, not a second documented
|
||||
syntax.
|
||||
Pydantic JSON schemas advertise both canonical path strings and structural
|
||||
`{root, parts}` objects. Serializers emit canonical strings. Validators continue
|
||||
to accept structural objects so machine clients and old persisted drafts,
|
||||
artifacts, and runs can use the explicit root/parts form; new public examples
|
||||
and writes prefer strings.
|
||||
|
||||
## Compatibility And Migration
|
||||
|
||||
@@ -346,7 +345,7 @@ shim is added without a real external caller.
|
||||
- an inferred route targets `__end__` unless explicitly overridden;
|
||||
- output binding still projects referenced schema definitions;
|
||||
- all path models parse and serialize the canonical TOML-key string grammar;
|
||||
- structural path objects remain readable but are not emitted;
|
||||
- structural path objects remain accepted input but are not emitted;
|
||||
- stored-workspace compile equals `compile_workflow_draft` output;
|
||||
- compile does not change revision, timestamps, status, or diagnostics.
|
||||
|
||||
|
||||
@@ -18,15 +18,18 @@ class InputPathBinding(BaseModel):
|
||||
|
||||
target: LocalPath = Field(
|
||||
description=(
|
||||
"Node-local input path to populate. Use {'root': 'local', "
|
||||
"'parts': ['field']} or {'root': 'local', 'parts': []} for the "
|
||||
"whole node input payload."
|
||||
"Node-local input path to populate. Prefer canonical strings such "
|
||||
"as `field` or `.` for the whole node input payload. Structural "
|
||||
"objects such as {'root': 'local', 'parts': ['field']} are also "
|
||||
"accepted as input."
|
||||
)
|
||||
)
|
||||
path: GraphSourcePath = Field(
|
||||
description=(
|
||||
"Workflow source path to read from input, state, or context. "
|
||||
"Example: {'root': 'input', 'parts': ['text']}."
|
||||
"Prefer canonical strings such as `input.text` or `state.report`. "
|
||||
"Structural objects such as {'root': 'input', 'parts': ['text']} "
|
||||
"are also accepted as input."
|
||||
)
|
||||
)
|
||||
|
||||
@@ -67,14 +70,17 @@ class OutputBinding(BaseModel):
|
||||
|
||||
source: LocalPath = Field(
|
||||
description=(
|
||||
"Node-local output path to read. Use {'root': 'local', 'parts': []} "
|
||||
"to write the whole node output payload."
|
||||
"Node-local output path to read. Prefer canonical strings such as "
|
||||
"`result` or `.` for the whole node output payload. Structural "
|
||||
"objects such as {'root': 'local', 'parts': []} are also accepted "
|
||||
"as input."
|
||||
)
|
||||
)
|
||||
target: StatePath = Field(
|
||||
description=(
|
||||
"Writable workflow state path. Bare state is invalid; use a field "
|
||||
"path such as {'root': 'state', 'parts': ['echoed']}."
|
||||
"path such as `state.echoed`. Structural objects such as "
|
||||
"{'root': 'state', 'parts': ['echoed']} are also accepted as input."
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+46
-6
@@ -78,15 +78,47 @@ def format_toml_path_segments(parts: tuple[str, ...]) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _path_json_schema(description: str) -> dict[str, Any]:
|
||||
"""Return the canonical string schema for path fields.
|
||||
def _path_json_schema(
|
||||
description: str, *, roots: tuple[str, ...], allow_empty_parts: bool
|
||||
) -> dict[str, Any]:
|
||||
"""Return the public schema for path fields.
|
||||
|
||||
Structural objects remain input-only compatibility for persisted records.
|
||||
New schemas and serializers expose the canonical TOML-key string form.
|
||||
Strings are the canonical serialized form. Structural objects are still a
|
||||
first-class input form, so the JSON Schema must advertise both; otherwise
|
||||
machine clients tend to quote structural objects and produce invalid TOML
|
||||
path strings.
|
||||
"""
|
||||
root_schema: dict[str, Any]
|
||||
if len(roots) == 1:
|
||||
root_schema = {"const": roots[0]}
|
||||
else:
|
||||
root_schema = {"enum": list(roots)}
|
||||
min_items = 0 if allow_empty_parts else 1
|
||||
return {
|
||||
"type": "string",
|
||||
"description": description,
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Canonical TOML-key path string.",
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"description": ("Structural path object accepted as input."),
|
||||
"additionalProperties": False,
|
||||
"required": ["root", "parts"],
|
||||
"properties": {
|
||||
"root": {
|
||||
"type": "string",
|
||||
**root_schema,
|
||||
},
|
||||
"parts": {
|
||||
"type": "array",
|
||||
"items": {"type": "string", "minLength": 1},
|
||||
"minItems": min_items,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -185,6 +217,8 @@ class LocalPath:
|
||||
) -> dict[str, Any]:
|
||||
return _path_json_schema(
|
||||
"Node-local path. Use the root marker `.` for the whole payload.",
|
||||
roots=("local",),
|
||||
allow_empty_parts=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -269,6 +303,8 @@ class GraphSourcePath:
|
||||
) -> dict[str, Any]:
|
||||
return _path_json_schema(
|
||||
"Readable graph path rooted at input, state, or context.",
|
||||
roots=("input", "state", "context"),
|
||||
allow_empty_parts=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -334,7 +370,11 @@ class StatePath:
|
||||
def __get_pydantic_json_schema__(
|
||||
cls, _core_schema: core_schema.CoreSchema, _handler: object
|
||||
) -> dict[str, Any]:
|
||||
return _path_json_schema("Writable workflow state path.")
|
||||
return _path_json_schema(
|
||||
"Writable workflow state path.",
|
||||
roots=("state",),
|
||||
allow_empty_parts=False,
|
||||
)
|
||||
|
||||
|
||||
def split_graph_path(path: str | GraphSourcePath | StatePath) -> tuple[str, list[str]]:
|
||||
|
||||
@@ -190,7 +190,7 @@ def test_pydantic_accepts_path_strings_and_serializes_path_strings() -> None:
|
||||
assert python_dumped["local"] == "user"
|
||||
|
||||
|
||||
def test_path_json_schema_advertises_string_type() -> None:
|
||||
def test_path_json_schema_advertises_strings_and_structural_objects() -> None:
|
||||
class Payload(BaseModel):
|
||||
source: GraphSourcePath
|
||||
target: StatePath
|
||||
@@ -198,9 +198,22 @@ def test_path_json_schema_advertises_string_type() -> None:
|
||||
|
||||
schema = Payload.model_json_schema()
|
||||
|
||||
assert schema["properties"]["source"]["type"] == "string"
|
||||
assert schema["properties"]["target"]["type"] == "string"
|
||||
assert schema["properties"]["local"]["type"] == "string"
|
||||
assert schema["properties"]["source"]["oneOf"][0]["type"] == "string"
|
||||
assert schema["properties"]["source"]["oneOf"][1]["properties"]["root"]["enum"] == [
|
||||
"input",
|
||||
"state",
|
||||
"context",
|
||||
]
|
||||
assert schema["properties"]["target"]["oneOf"][0]["type"] == "string"
|
||||
assert (
|
||||
schema["properties"]["target"]["oneOf"][1]["properties"]["root"]["const"]
|
||||
== "state"
|
||||
)
|
||||
assert schema["properties"]["local"]["oneOf"][0]["type"] == "string"
|
||||
assert (
|
||||
schema["properties"]["local"]["oneOf"][1]["properties"]["root"]["const"]
|
||||
== "local"
|
||||
)
|
||||
|
||||
|
||||
def test_condition_path_operand_serializes_path_as_string() -> None:
|
||||
@@ -354,4 +367,15 @@ def test_path_models_serialize_strings_but_accept_structural_compat() -> None:
|
||||
"target": 'state."person name"',
|
||||
"local": '"payload.text"',
|
||||
}
|
||||
assert Payload.model_json_schema()["properties"]["source"]["type"] == "string"
|
||||
assert (
|
||||
Payload.model_json_schema()["properties"]["source"]["oneOf"][0]["type"]
|
||||
== "string"
|
||||
)
|
||||
|
||||
|
||||
def test_json_encoded_structural_path_string_is_invalid() -> None:
|
||||
class Payload(BaseModel):
|
||||
source: GraphSourcePath
|
||||
|
||||
with pytest.raises(ValidationError, match="invalid TOML path"):
|
||||
Payload.model_validate({"source": '{"root":"input","parts":["button_label"]}'})
|
||||
|
||||
Reference in New Issue
Block a user