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
|
- Completed: challenge matrix operations now have compact OpenCode thread
|
||||||
titles, policy handling for canonical skill-document reads, and a central
|
titles, policy handling for canonical skill-document reads, and a central
|
||||||
`summarize_trials.py` command for audited result tables.
|
`summarize_trials.py` command for audited result tables.
|
||||||
- Completed: canonical TOML path strings replace structural JSON path objects
|
- Completed: canonical TOML path strings are the emitted workflow path form.
|
||||||
in workflow drafts. Paths now emit as `"input.text"`, `"state.echoed"`,
|
Paths now serialize as `"input.text"`, `"state.echoed"`, and `"message"`
|
||||||
`"message"` (local) instead of `{"root": "input", "parts": ["text"]}`.
|
(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
|
- Completed: challenge-driven CLI UX fixes now provide exact available
|
||||||
deployment binding suggestions, reject bare `--bind-output` state targets
|
deployment binding suggestions, reject bare `--bind-output` state targets
|
||||||
before RPC with compact guidance, and accept `wf schema --full` as an alias
|
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"
|
"input.message"
|
||||||
```
|
```
|
||||||
|
|
||||||
Structural root/parts objects are still accepted at model-parse boundaries for
|
Structural root/parts objects are still accepted at model-parse boundaries and
|
||||||
old persisted records, but new schemas and new examples should emit strings.
|
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
|
Quote a segment when the field name itself contains a dot or space, for example
|
||||||
`state."person.name"` or `state.person."full name"`.
|
`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
|
spaces, and other non-bare segments. Parse errors identify the complete input
|
||||||
and recommend quoting the invalid segment.
|
and recommend quoting the invalid segment.
|
||||||
|
|
||||||
Pydantic JSON schemas advertise path strings rather than the structural
|
Pydantic JSON schemas advertise both canonical path strings and structural
|
||||||
`{root, parts}` object. Serializers emit canonical strings. Validators continue
|
`{root, parts}` objects. Serializers emit canonical strings. Validators continue
|
||||||
to accept the structural object only as a read-compatibility path for existing
|
to accept structural objects so machine clients and old persisted drafts,
|
||||||
persisted drafts, artifacts, and runs; new public examples and writes use
|
artifacts, and runs can use the explicit root/parts form; new public examples
|
||||||
strings. This is compatibility for real stored data, not a second documented
|
and writes prefer strings.
|
||||||
syntax.
|
|
||||||
|
|
||||||
## Compatibility And Migration
|
## Compatibility And Migration
|
||||||
|
|
||||||
@@ -346,7 +345,7 @@ shim is added without a real external caller.
|
|||||||
- an inferred route targets `__end__` unless explicitly overridden;
|
- an inferred route targets `__end__` unless explicitly overridden;
|
||||||
- output binding still projects referenced schema definitions;
|
- output binding still projects referenced schema definitions;
|
||||||
- all path models parse and serialize the canonical TOML-key string grammar;
|
- 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;
|
- stored-workspace compile equals `compile_workflow_draft` output;
|
||||||
- compile does not change revision, timestamps, status, or diagnostics.
|
- compile does not change revision, timestamps, status, or diagnostics.
|
||||||
|
|
||||||
|
|||||||
@@ -18,15 +18,18 @@ class InputPathBinding(BaseModel):
|
|||||||
|
|
||||||
target: LocalPath = Field(
|
target: LocalPath = Field(
|
||||||
description=(
|
description=(
|
||||||
"Node-local input path to populate. Use {'root': 'local', "
|
"Node-local input path to populate. Prefer canonical strings such "
|
||||||
"'parts': ['field']} or {'root': 'local', 'parts': []} for the "
|
"as `field` or `.` for the whole node input payload. Structural "
|
||||||
"whole node input payload."
|
"objects such as {'root': 'local', 'parts': ['field']} are also "
|
||||||
|
"accepted as input."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
path: GraphSourcePath = Field(
|
path: GraphSourcePath = Field(
|
||||||
description=(
|
description=(
|
||||||
"Workflow source path to read from input, state, or context. "
|
"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(
|
source: LocalPath = Field(
|
||||||
description=(
|
description=(
|
||||||
"Node-local output path to read. Use {'root': 'local', 'parts': []} "
|
"Node-local output path to read. Prefer canonical strings such as "
|
||||||
"to write the whole node output payload."
|
"`result` or `.` for the whole node output payload. Structural "
|
||||||
|
"objects such as {'root': 'local', 'parts': []} are also accepted "
|
||||||
|
"as input."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
target: StatePath = Field(
|
target: StatePath = Field(
|
||||||
description=(
|
description=(
|
||||||
"Writable workflow state path. Bare state is invalid; use a field "
|
"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]:
|
def _path_json_schema(
|
||||||
"""Return the canonical string schema for path fields.
|
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.
|
Strings are the canonical serialized form. Structural objects are still a
|
||||||
New schemas and serializers expose the canonical TOML-key string form.
|
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 {
|
return {
|
||||||
"type": "string",
|
|
||||||
"description": description,
|
"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]:
|
) -> dict[str, Any]:
|
||||||
return _path_json_schema(
|
return _path_json_schema(
|
||||||
"Node-local path. Use the root marker `.` for the whole payload.",
|
"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]:
|
) -> dict[str, Any]:
|
||||||
return _path_json_schema(
|
return _path_json_schema(
|
||||||
"Readable graph path rooted at input, state, or context.",
|
"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__(
|
def __get_pydantic_json_schema__(
|
||||||
cls, _core_schema: core_schema.CoreSchema, _handler: object
|
cls, _core_schema: core_schema.CoreSchema, _handler: object
|
||||||
) -> dict[str, Any]:
|
) -> 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]]:
|
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"
|
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):
|
class Payload(BaseModel):
|
||||||
source: GraphSourcePath
|
source: GraphSourcePath
|
||||||
target: StatePath
|
target: StatePath
|
||||||
@@ -198,9 +198,22 @@ def test_path_json_schema_advertises_string_type() -> None:
|
|||||||
|
|
||||||
schema = Payload.model_json_schema()
|
schema = Payload.model_json_schema()
|
||||||
|
|
||||||
assert schema["properties"]["source"]["type"] == "string"
|
assert schema["properties"]["source"]["oneOf"][0]["type"] == "string"
|
||||||
assert schema["properties"]["target"]["type"] == "string"
|
assert schema["properties"]["source"]["oneOf"][1]["properties"]["root"]["enum"] == [
|
||||||
assert schema["properties"]["local"]["type"] == "string"
|
"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:
|
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"',
|
"target": 'state."person name"',
|
||||||
"local": '"payload.text"',
|
"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