fix: smooth challenge-driven cli ux gaps

This commit is contained in:
lda
2026-06-28 23:18:33 +07:00 Verified
parent 8b751da402
commit bf9abda1c3
10 changed files with 164 additions and 7 deletions
+4
View File
@@ -271,6 +271,10 @@ stable.
- 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: 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
for `--verbose`.
## Historical References
+15
View File
@@ -134,6 +134,18 @@ availability, durable run counts/latest run, admin counts, auth record count,
and desired registry count when the target exposes those surfaces. It does not
return auth payload values, trace entries, or checkpoint state.
Schema discovery:
```bash
wf schema
wf schema draft
wf schema raw --verbose
wf schema raw --full
```
`--full` is an alias for `--verbose`; docs use `--verbose` as the canonical
flag.
### Diagnose A Source
Use `wf source diagnose <source_id>` to inspect source health before calling
@@ -477,6 +489,9 @@ wf artifact create-from-plan workflow.plan.json \
Prefer draft workspaces for iterative authoring. Use `create-from-plan` when a
compiler, fixture, or advanced client already has a complete raw workflow plan.
Artifact save responses include `required_logical_sources` and may include
`suggested_bindings`. Copy suggested bindings into `wf deploy save --binding`
when they are present; otherwise choose the concrete source/account explicitly.
Delete an unreferenced artifact version:
+5 -2
View File
@@ -76,7 +76,8 @@ Use public CLI surfaces before broader documentation or implementation search:
6. `wf explain <diagnostic-code>` after validation failures
Use `wf schema <name> --verbose` only when the complete JSON Schema is required;
the default compact outline is preferred for agent context.
the default compact outline is preferred for agent context. `--full` is accepted
as an alias for `--verbose`.
For `draft set-input` and `draft set-output`, repeated `--map` flags in one
command define the complete replacement map. If you split map edits across
@@ -121,13 +122,15 @@ top-level `compiled_plan` key from the CLI output.
- Use `wf schema draft`, `wf schema raw`, or `wf schema <Component>` for compact
JSON guidance before authoring.
- Add `--verbose` only when a complete JSON Schema document is required; it may
be large.
be large. `--full` is an alias if you already tried that spelling.
- Prefer `wf schema` over searching tests or implementation code for draft/raw
plan shape.
- Treat compact schema output as authoring guidance; use validation commands as
the source of truth for a concrete document.
- If public commands and supplied skills are insufficient, report the exact
blocker instead of guessing undocumented fields.
- When artifact save returns `suggested_bindings`, copy those values into
`wf deploy save --binding`; otherwise choose the concrete source explicitly.
- `status: invalid` from a draft edit is not always a command failure. Inspect
diagnostics and continue repairing the same workspace unless the command
reports a conflict or malformed patch.
@@ -69,6 +69,19 @@ wf deploy validate <deployment_id>
wf run start <deployment_id> --input-file input.json
```
If artifact save returns `suggested_bindings`, include each suggestion as a
deployment binding:
```bash
wf deploy save <deployment_id> \
--artifact <artifact_id> \
--version 1 \
--binding local.report=local.report
```
If no suggestion is present, do not guess an account-like source binding; inspect
sources and choose the concrete source explicitly.
## Object Model
- **Source**: owner of capabilities, such as `wf.std` or `everything.default`.
+21 -4
View File
@@ -199,7 +199,10 @@ class WorkflowArtifactApi:
"version": workflow_artifact.version,
"saved": True,
"required_logical_sources": required_sources,
"suggested_bindings": _suggested_self_bindings(required_sources),
"suggested_bindings": _suggested_self_bindings(
required_sources,
self.context.specs.capability_sources,
),
}
async def create_artifact_from_workspace(
@@ -302,9 +305,23 @@ class WorkflowArtifactApi:
}
def _suggested_self_bindings(required_sources: Sequence[str]) -> dict[str, str]:
"""Suggest local bindings for external sources that deploy to themselves."""
return {}
def _suggested_self_bindings(
required_sources: Sequence[str],
sources: Mapping[str, CapabilitySource],
) -> dict[str, str]:
"""Suggest only exact, enabled concrete sources as deployment bindings.
Ambiguous logical sources such as ``drive`` with multiple concrete accounts
must stay unsuggested. Exact ids such as ``local.report`` are safe because
the same source is already present in the active inventory.
"""
return {
source_id: source_id
for source_id in required_sources
if (source := sources.get(source_id)) is not None
and source.enabled
and source.policy.binding_required
}
def _binding_required_sources(
+26 -1
View File
@@ -10,6 +10,7 @@ from wf_cli.context import load_cli_context_from_typer as load_cli_context
from wf_cli.formats import ListOutputFormat, emit_list_payload
from wf_cli.io import CliInputError, emit_json, parse_bindings, parse_json_value
from wf_cli.remote_errors import run_cli_operation
from wf_core.paths import LocalPath, PathResolutionError, StatePath
def _parse_assignment_flags(
@@ -37,6 +38,30 @@ def _parse_map_flags(values: list[str] | None) -> dict[str, str]:
)
def _parse_output_map_flags(values: list[str] | None) -> dict[str, str]:
parsed = _parse_assignment_flags(
values,
option_name="--bind-output",
expected="LOCAL_OUTPUT=STATE_TARGET",
)
for local_output, state_target in parsed.items():
try:
LocalPath.parse(local_output)
except PathResolutionError as exc:
raise typer.BadParameter(
f"--bind-output source {local_output!r} must be a node-local "
"output path such as value or ."
) from exc
try:
StatePath.parse(state_target)
except PathResolutionError as exc:
raise typer.BadParameter(
f"--bind-output target {state_target!r} must be a state path "
"such as state.value"
) from exc
return parsed
def _parse_route_flags(values: list[str] | None) -> dict[str, str]:
return _parse_assignment_flags(
values,
@@ -396,7 +421,7 @@ def add_step_from_capability(
want, then run `wf draft validate <workspace_id>`.
"""
input_map = _parse_map_flags(input_mapping)
bind_outputs = _parse_map_flags(output_mapping)
bind_outputs = _parse_output_map_flags(output_mapping)
routes = _parse_route_flags(route)
context = load_cli_context(ctx)
emit_json(
+1
View File
@@ -19,6 +19,7 @@ def schema_command(
verbose: bool = typer.Option(
False,
"--verbose",
"--full",
"-v",
help="Print complete valid JSON Schema; output may be large.",
),
+26
View File
@@ -185,6 +185,32 @@ async def test_create_artifact_from_plan_saves_with_observed_node_specs(
assert saved.id == "echo"
@pytest.mark.asyncio
async def test_create_artifact_from_workspace_suggests_exact_available_source_binding(
tmp_path: Path,
) -> None:
artifact_store = FileWorkflowArtifactStore(tmp_path / "artifacts_binding_hint")
api, service = _artifact_api(artifact_store, register_echo=True)
from wf_api.drafts import WorkflowDraftApi
drafts_api = WorkflowDraftApi(context_from_service(service))
await drafts_api.create_draft_workspace(
workspace_id="echo_ws",
draft=_echo_draft(),
)
result = await api.create_artifact_from_workspace(
workspace_id="echo_ws",
artifact_id="echo",
version=1,
title="Echo",
outcomes=("completed",),
)
assert result["required_logical_sources"] == ["demo.personal"]
assert result["suggested_bindings"] == {"demo.personal": "demo.personal"}
async def test_create_artifact_from_workspace_returns_saved_false_when_invalid(
tmp_path: Path,
) -> None:
+49
View File
@@ -1290,6 +1290,55 @@ def test_wf_draft_add_step_from_capability_uses_rpc_target(
assert payload["status"] == "valid"
def test_wf_draft_add_step_reports_bare_output_target_without_traceback(
monkeypatch, tmp_path
) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
config_path = tmp_path / "wf.json"
config_path.write_text('{"version": 1}', encoding="utf-8")
runner = CliRunner()
base_args = ["--config", str(config_path), "--url", "http://test/rpc"]
created = runner.invoke(
app,
[
*base_args,
"draft",
"create",
"add_step_ws",
"--capability",
"wf.std.constant",
"--name",
"add_step",
],
)
assert created.exit_code == 0, created.output
result = runner.invoke(
app,
[
*base_args,
"draft",
"add-step",
"add_step_ws",
"--revision",
"1",
"--step",
"second",
"--capability",
"wf.std.constant",
"--bind-output",
"value=value",
],
)
assert result.exit_code != 0
assert "Traceback" not in result.output
assert "--bind-output" in result.output
assert "state.value" in result.output
def test_wf_draft_compile_prints_compiled_plan(monkeypatch, tmp_path) -> None:
server = build_local_static_workflow_server(tmp_path / "store")
_patch_rpc_client_to_server(monkeypatch, server)
+4
View File
@@ -96,6 +96,10 @@ def test_schema_verbose_root_is_valid_json_schema() -> None:
assert payload["title"] == "RawWorkflowPlan"
def test_schema_full_alias_matches_verbose() -> None:
assert _json_result("raw", "--full") == _json_result("raw", "--verbose")
def test_schema_verbose_component_is_self_contained() -> None:
payload = _json_result("NodeUse", "--verbose")