CANP-WP-0002 T01: input defaults, static and derived
Closes the gap that made optional inputs unusable: rendering rule 5.1(4) made any unresolved placeholder an error while inputs had no `default`, so an input marked `required: false` and referenced from the template failed every render in which the caller omitted it — including the spec's own section 4 example. Section 10 already carried the derivation mechanism (`requirement: generate`, resolution deliberately undefined), so a derived default needed a binding rather than a new concept: the input's default names a declared prompt dependency. Spec: - 5.1 rewritten as "Resolution and rendering". Resolution may be non-deterministic and must report what it derived; rendering is deterministic and must not derive. A tool that handles only supplied values and static defaults is stated to be conforming. - 6.1 (new) covers both declaration forms. Reference form is preferred, with the reason stated — an inline prompt is anonymous, so unversioned, unprovenanced and un-evaluable — and validators should warn when a published package derives inline. - A derived default may declare a static fallback `value`. Without one the input stays unresolved, which is an error; derivation never silently yields empty content. - 4, 10, 18 (rules 11-13), 19 (two new MUST NOTs), 21 updated accordingly. Reference CLI: - New `resolve` verb reporting the origin of every value. - `Resolution` dataclass and `resolve_inputs`; `resolve_values` kept as a wrapper so existing callers are unaffected. - `render` refuses with a specific error naming underivable inputs rather than substituting empty text. - Tests 3 -> 11. Example package lifecycle re-verified end to end. INTENT.md is unchanged: splitting resolve from render preserves success criterion 4 (deterministic rendering) as written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjefh8NUiEiahN4JLwoSKM Assistant: claude-code Assistant-Model: opus Assistant-Process: 388925@bnt-lap001 Assistant-Session: 3507023f-e0fd-4a1e-9d90-a0d4217d1502
This commit is contained in:
parent
0caf065544
commit
169db25d25
6 changed files with 600 additions and 36 deletions
|
|
@ -51,3 +51,172 @@ def test_undeclared_placeholder_fails(package: Path) -> None:
|
|||
(package / "prompt.md").write_text("{{ missing }}\n", encoding="utf-8")
|
||||
with pytest.raises(cp.CannedPromptError, match="undeclared placeholders"):
|
||||
cp.validate_package(package)
|
||||
|
||||
|
||||
def write_pkg(pkg: Path, manifest: str, template: str = "{{ greeting }}\n") -> Path:
|
||||
pkg.mkdir(exist_ok=True)
|
||||
(pkg / "prompt.yaml").write_text(manifest, encoding="utf-8")
|
||||
(pkg / "prompt.md").write_text(template, encoding="utf-8")
|
||||
return pkg
|
||||
|
||||
|
||||
BASE = """\
|
||||
format: canned-prompt/v0.1
|
||||
id: demo/defaults
|
||||
name: Defaults
|
||||
version: 1.0.0
|
||||
summary: Exercise input defaults.
|
||||
template: prompt.md
|
||||
"""
|
||||
|
||||
|
||||
def test_static_default_fills_optional_input(tmp_path: Path) -> None:
|
||||
pkg = write_pkg(
|
||||
tmp_path / "p",
|
||||
BASE
|
||||
+ """\
|
||||
inputs:
|
||||
- name: greeting
|
||||
required: false
|
||||
default: "hello there"
|
||||
""",
|
||||
)
|
||||
manifest = cp.validate_package(pkg)
|
||||
resolution = cp.resolve_inputs(manifest, {})
|
||||
assert resolution.values["greeting"] == "hello there"
|
||||
assert resolution.origins["greeting"] == "default"
|
||||
assert cp.render_template("{{ greeting }}", resolution.values) == "hello there"
|
||||
|
||||
|
||||
def test_supplied_value_overrides_static_default(tmp_path: Path) -> None:
|
||||
pkg = write_pkg(
|
||||
tmp_path / "p",
|
||||
BASE
|
||||
+ """\
|
||||
inputs:
|
||||
- name: greeting
|
||||
required: false
|
||||
default: "hello there"
|
||||
""",
|
||||
)
|
||||
manifest = cp.validate_package(pkg)
|
||||
resolution = cp.resolve_inputs(manifest, {"greeting": "hi"})
|
||||
assert resolution.values["greeting"] == "hi"
|
||||
assert resolution.origins["greeting"] == "supplied"
|
||||
|
||||
|
||||
def test_derived_default_uses_static_fallback(tmp_path: Path) -> None:
|
||||
pkg = write_pkg(
|
||||
tmp_path / "p",
|
||||
BASE
|
||||
+ """\
|
||||
dependencies:
|
||||
prompts:
|
||||
- id: context/greeting
|
||||
version: 1.0.0
|
||||
requirement: generate
|
||||
inputs:
|
||||
- name: greeting
|
||||
required: false
|
||||
default:
|
||||
derive: context/greeting
|
||||
value: "(none)"
|
||||
""",
|
||||
)
|
||||
manifest = cp.validate_package(pkg)
|
||||
resolution = cp.resolve_inputs(manifest, {})
|
||||
assert resolution.values["greeting"] == "(none)"
|
||||
assert resolution.origins["greeting"] == "fallback (not derived)"
|
||||
assert resolution.underivable == []
|
||||
|
||||
|
||||
def test_derived_default_without_fallback_is_underivable(tmp_path: Path) -> None:
|
||||
pkg = write_pkg(
|
||||
tmp_path / "p",
|
||||
BASE
|
||||
+ """\
|
||||
dependencies:
|
||||
prompts:
|
||||
- id: context/greeting
|
||||
version: 1.0.0
|
||||
requirement: generate
|
||||
inputs:
|
||||
- name: greeting
|
||||
required: false
|
||||
default:
|
||||
derive: context/greeting
|
||||
""",
|
||||
)
|
||||
manifest = cp.validate_package(pkg)
|
||||
resolution = cp.resolve_inputs(manifest, {})
|
||||
assert resolution.underivable == ["greeting"]
|
||||
assert "greeting" not in resolution.values
|
||||
with pytest.raises(cp.CannedPromptError, match="unresolved placeholder"):
|
||||
cp.render_template("{{ greeting }}", resolution.values)
|
||||
|
||||
|
||||
def test_inline_derive_is_valid(tmp_path: Path) -> None:
|
||||
pkg = write_pkg(
|
||||
tmp_path / "p",
|
||||
BASE
|
||||
+ """\
|
||||
inputs:
|
||||
- name: greeting
|
||||
required: false
|
||||
default:
|
||||
derive:
|
||||
prompt: Produce a greeting suited to the audience.
|
||||
value: "(none)"
|
||||
""",
|
||||
)
|
||||
manifest = cp.validate_package(pkg)
|
||||
assert cp.resolve_inputs(manifest, {}).values["greeting"] == "(none)"
|
||||
|
||||
|
||||
def test_default_with_required_true_fails(tmp_path: Path) -> None:
|
||||
pkg = write_pkg(
|
||||
tmp_path / "p",
|
||||
BASE
|
||||
+ """\
|
||||
inputs:
|
||||
- name: greeting
|
||||
required: true
|
||||
default: "hello"
|
||||
""",
|
||||
)
|
||||
with pytest.raises(cp.CannedPromptError, match="required: true"):
|
||||
cp.validate_package(pkg)
|
||||
|
||||
|
||||
def test_derive_reference_must_be_declared(tmp_path: Path) -> None:
|
||||
pkg = write_pkg(
|
||||
tmp_path / "p",
|
||||
BASE
|
||||
+ """\
|
||||
inputs:
|
||||
- name: greeting
|
||||
required: false
|
||||
default:
|
||||
derive: context/greeting
|
||||
""",
|
||||
)
|
||||
with pytest.raises(cp.CannedPromptError, match="not declared in dependencies"):
|
||||
cp.validate_package(pkg)
|
||||
|
||||
|
||||
def test_inline_derive_cannot_also_reference(tmp_path: Path) -> None:
|
||||
pkg = write_pkg(
|
||||
tmp_path / "p",
|
||||
BASE
|
||||
+ """\
|
||||
inputs:
|
||||
- name: greeting
|
||||
required: false
|
||||
default:
|
||||
derive:
|
||||
prompt: Produce a greeting.
|
||||
id: context/greeting
|
||||
""",
|
||||
)
|
||||
with pytest.raises(cp.CannedPromptError, match="cannot also reference"):
|
||||
cp.validate_package(pkg)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue