Register with Custodian State Hub and seed format open-questions workplan
Register canned-prompts under agents / practice (topic
c1d199b6-55ee-4db6-b49e-257a9f0f15ac, workplan prefix CANP-WP) via
`statehub register`, then replace the generated placeholders with
repo-specific facts.
- SCOPE.md: real boundaries drawn from INTENT.md's deliberate boundary,
current state (spec v0.1 + reference CLI, 3/3 tests pass, example
round-trips), and the developer workflow.
- AGENTS.md: drop the unresolved {CREDENTIAL_ROUTING} template token left
by the generator.
- CANP-WP-0001: bootstrap tasks closed.
- CANP-WP-0002: new workplan carrying the five § 23 open questions promoted
from "experience will decide" to "decide for v0.2" — optional-input
defaults (static or derived), registry namespaces/ownership, prompt
composition, canonical eval schemas, typed context/dependency contracts —
plus two reference-implementation conformance defects found in review
(prerelease versions sort as newest; copy_immutable packages the whole
source directory).
Also lands the previously untracked seed: INTENT.md, the CPF v0.1 spec,
the reference CLI, and examples/pqrst-estimate.
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
2026-09-06 00:45:23 +02:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
import canned_prompts as cp
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture()
|
|
|
|
|
def package(tmp_path: Path) -> Path:
|
|
|
|
|
pkg = tmp_path / "pkg"
|
|
|
|
|
pkg.mkdir()
|
|
|
|
|
(pkg / "prompt.yaml").write_text(
|
|
|
|
|
"""\
|
|
|
|
|
format: canned-prompt/v0.1
|
|
|
|
|
id: demo/hello
|
|
|
|
|
name: Hello
|
|
|
|
|
version: 1.0.0
|
|
|
|
|
summary: Say hello.
|
|
|
|
|
template: prompt.md
|
|
|
|
|
inputs:
|
|
|
|
|
- name: person
|
|
|
|
|
required: true
|
|
|
|
|
parameters:
|
|
|
|
|
tone:
|
|
|
|
|
type: enum
|
|
|
|
|
values: [warm, formal]
|
|
|
|
|
default: warm
|
|
|
|
|
""",
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
(pkg / "prompt.md").write_text(
|
|
|
|
|
"Say hello to {{ person }} in a {{ tone }} tone.\n", encoding="utf-8"
|
|
|
|
|
)
|
|
|
|
|
return pkg
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_validate_and_render(package: Path) -> None:
|
|
|
|
|
manifest = cp.validate_package(package)
|
|
|
|
|
values = cp.resolve_values(manifest, {"person": "Ada"})
|
|
|
|
|
rendered = cp.render_template((package / "prompt.md").read_text(), values)
|
|
|
|
|
assert rendered == "Say hello to Ada in a warm tone.\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_missing_required_input_fails(package: Path) -> None:
|
|
|
|
|
manifest = cp.validate_package(package)
|
|
|
|
|
with pytest.raises(cp.CannedPromptError, match="missing required input"):
|
|
|
|
|
cp.resolve_values(manifest, {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
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
2026-09-06 00:59:20 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|