CANP-WP-0002 T05: required versus observed, and typed context dependencies

The answer to the capability question was conditional: keep both fields if
they carry the required/observed distinction, fix the terminology if they do
not. They did not.

Section 9 opened with "records known requirements or observations", mixing
both in one field — `models` was observational ("known to be compatible or
evaluated") while `capabilities` was prescriptive ("expected from the
execution environment"). Section 10 then described dependencies as what a
prompt "expects". Both fields said expected, so the overlap was real
ambiguity rather than redundancy, and the fix is terminology.

`dependencies` now means **required**; `compatibility` means **observed**. A
consumer must not refuse to run a package because its environment is absent
from a compatibility list. The same capability name may legitimately appear in
both: required to run at all, and separately observed to work well on
particular models. `compatibility.aliases` records the same capability under
other names, so a consumer can recognize a requirement its environment labels
differently.

Dependencies now have three kinds, separated by what the format can do about
them: `prompts` it resolves by id and version; `context` names what it does
not package at all; `capabilities` are what the environment must be able to
do. Context entries use `name` rather than `id`, because nothing can look them
up, and `description` is required because nothing else can explain an
unpackaged dependency. A capability takes no version and no
`requirement: generate` — it is not an artifact and cannot be fetched, pinned
or generated. Capability names are free-form kebab-case, validated for shape
and not membership, exactly as tags are.

Section 10.1 also draws the line the format had never stated: an input is
content the caller passes for one use; a context dependency is a standing fact
about the environment.

Spec: 9 rewritten, 9.1 and 10.1 and 10.2 new, 10 reframed, 18 (rules 19-20),
4 updated. Former 10.1/10.2 renumbered to 10.3/10.4 with cross-references.

Reference CLI: validate_capabilities, validate_context_dependencies, and a
`resolve` section listing required capabilities and context under "this tool
cannot verify these" rather than implying it checked. Tests 51 -> 65.

Also drops an invented `session-review` capability from the example package in
favour of an honest `long-context` observation.

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:
tegwick 2026-09-06 08:11:13 +02:00
parent 074fd79d53
commit c580bf63c9
8 changed files with 323 additions and 32 deletions

View file

@ -615,3 +615,68 @@ def test_typed_fixture_values_are_not_reparsed(tmp_path: Path) -> None:
"""A YAML fixture carries real types; only CLI strings need parsing."""
manifest = {"parameters": {"flag": {"type": "boolean", "default": False}}}
assert cp.resolve_inputs(manifest, {"flag": True}).values["flag"] is True
# --- context and capability dependencies (§ 10.1, § 10.2) ---
def ctx_pkg(tmp_path: Path, body: str) -> Path:
return write_pkg(tmp_path / "p", BASE + body, "hello\n")
def test_context_dependency_requires_name_and_description(tmp_path: Path) -> None:
pkg = ctx_pkg(
tmp_path,
"dependencies:\n context:\n - name: repository-tree\n"
" description: A listing of the repository.\n",
)
assert cp.validate_package(pkg)["id"] == "demo/defaults"
def test_context_dependency_without_description_is_rejected(tmp_path: Path) -> None:
pkg = ctx_pkg(tmp_path, "dependencies:\n context:\n - name: repository-tree\n")
with pytest.raises(cp.CannedPromptError, match="must declare a description"):
cp.validate_package(pkg)
def test_context_dependency_using_id_is_rejected(tmp_path: Path) -> None:
"""Context entries use `name`; `id` would imply resolvable package identity."""
pkg = ctx_pkg(
tmp_path,
"dependencies:\n context:\n - id: policy/security\n"
" description: A policy.\n",
)
with pytest.raises(cp.CannedPromptError, match="not 'id'"):
cp.validate_package(pkg)
def test_context_requirement_generate_is_rejected(tmp_path: Path) -> None:
pkg = ctx_pkg(
tmp_path,
"dependencies:\n context:\n - name: repository-tree\n"
" description: A listing.\n requirement: generate\n",
)
with pytest.raises(cp.CannedPromptError, match="required' or 'optional"):
cp.validate_package(pkg)
@pytest.mark.parametrize("name", ["web-search", "code-analysis", "vision", "a1-b2"])
def test_valid_capability_names(name) -> None:
assert cp.validate_capabilities([name], "dependencies.capabilities") == [name]
@pytest.mark.parametrize("name", ["Web-Search", "web_search", "-lead", "trail-", ""])
def test_invalid_capability_names(name) -> None:
with pytest.raises(cp.CannedPromptError, match="kebab-case"):
cp.validate_capabilities([name], "dependencies.capabilities")
def test_capability_may_be_both_required_and_observed(tmp_path: Path) -> None:
"""§ 9.1: required to run at all, and separately observed to work well."""
pkg = ctx_pkg(
tmp_path,
"dependencies:\n capabilities:\n - web-search\n"
"compatibility:\n capabilities:\n - web-search\n - long-context\n",
)
manifest = cp.validate_package(pkg)
assert manifest["dependencies"]["capabilities"] == ["web-search"]
assert "long-context" in manifest["compatibility"]["capabilities"]