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

@ -181,7 +181,7 @@ Declares what kind of artifact the package is. Optional; defaults to
| Value | Meaning | | Value | Meaning |
|---|---| |---|---|
| `template` | A complete prompt, intended to be used on its own | | `template` | A complete prompt, intended to be used on its own |
| `fragment` | A reusable block intended for inclusion in other packages (§ 10.1) | | `fragment` | A reusable block intended for inclusion in other packages (§ 10.4) |
`type` is advisory. A fragment is a perfectly valid package and MAY be `type` is advisory. A fragment is a perfectly valid package and MAY be
rendered on its own; the field records the author's intent so that a consumer rendered on its own; the field records the author's intent so that a consumer
@ -233,7 +233,7 @@ output:
compatibility: compatibility:
capabilities: capabilities:
- code-analysis - long-context
models: [] models: []
providers: [] providers: []
@ -242,8 +242,12 @@ dependencies:
- id: context/repository-summary - id: context/repository-summary
version: 1.0.0 version: 1.0.0
requirement: generate requirement: generate
context: [] context:
capabilities: [] - name: repository-tree
description: A listing of the repository under review.
requirement: optional
capabilities:
- code-analysis
examples: examples:
- examples/basic.yaml - examples/basic.yaml
@ -313,7 +317,7 @@ always produce the same output. A consumer that satisfies a derived default
static fallback `value` when one is declared, and otherwise leaves the static fallback `value` when one is declared, and otherwise leaves the
input unresolved. input unresolved.
6. An included input default is satisfied by rendering the included package 6. An included input default is satisfied by rendering the included package
(§ 10.2). This is deterministic, so a consumer that can render can satisfy (§ 10.4). This is deterministic, so a consumer that can render can satisfy
it; one that cannot locate the package uses the static fallback `value` it; one that cannot locate the package uses the static fallback `value`
when declared, and otherwise leaves the input unresolved. when declared, and otherwise leaves the input unresolved.
7. Resolution MUST report which values were derived or included, so that a 7. Resolution MUST report which values were derived or included, so that a
@ -449,7 +453,7 @@ and draft packages.
#### Included default #### Included default
A default may instead **include** another package's rendered template as text A default may instead **include** another package's rendered template as text
(§ 10.2). Unlike a derived default this is deterministic and needs no model, (§ 10.4). Unlike a derived default this is deterministic and needs no model,
so every implementation that can render can also include: so every implementation that can render can also include:
```yaml ```yaml
@ -538,13 +542,18 @@ Registries MAY index output format for discovery.
## 9. Compatibility ## 9. Compatibility
`compatibility` records known requirements or observations without binding the package to one runtime. `compatibility` records **observations**, never requirements.
Everything here describes where the package has been seen to work. Nothing here
is a precondition for using it, and a consumer MUST NOT refuse to run a package
because its environment is absent from these lists.
```yaml ```yaml
compatibility: compatibility:
capabilities: capabilities:
- code-analysis
- long-context - long-context
aliases:
web-search: [browsing, tool-search]
models: models:
- example/model-family - example/model-family
providers: [] providers: []
@ -552,15 +561,43 @@ compatibility:
Semantics: Semantics:
- `capabilities`: abstract capabilities expected from the execution environment; - `capabilities`: capabilities the package is known to work well with, whether
or not it requires them;
- `aliases`: the same capability as known by other names, so that a consumer
can recognize a requirement its environment labels differently;
- `models`: model identifiers known to be compatible or evaluated; - `models`: model identifiers known to be compatible or evaluated;
- `providers`: provider identifiers when provider-specific behavior matters. - `providers`: provider identifiers when provider-specific behavior matters.
An empty list means "not constrained/unspecified", not "compatible with nothing". An empty list means "not constrained/unspecified", not "compatible with nothing".
### 9.1 Required versus compatible
Earlier drafts described `compatibility` as recording "requirements or
observations" and `dependencies` as recording what a prompt "expects", so both
appeared to say the same thing about capabilities. They do not, and the
distinction is worth stating in one word each:
| Field | Meaning | Absence means |
|---|---|---|
| `dependencies` | **required** — the package does not work without it | a consumer SHOULD warn or refuse before running |
| `compatibility` | **observed** — the package is known to work with it | nothing; it is information, not a gate |
A capability the prompt cannot function without belongs in
`dependencies.capabilities`. A capability it merely benefits from, or has been
evaluated against, belongs in `compatibility.capabilities`. The same name may
legitimately appear in both: required to run at all, and separately observed to
work well on particular models.
## 10. Dependencies ## 10. Dependencies
Dependencies describe external artifacts or capabilities expected by the prompt. Dependencies describe what a package **requires** in order to work (§ 9.1).
There are three kinds, separated by what CPF can do about them:
| Kind | Names | CPF can resolve it |
|---|---|---|
| `prompts` | other CPF packages | yes — by id and version (§ 10.3) |
| `context` | things CPF does not package | no — the consumer supplies them |
| `capabilities` | what the environment must be able to do | no — the environment either can or cannot |
```yaml ```yaml
dependencies: dependencies:
@ -570,13 +607,55 @@ dependencies:
requirement: optional requirement: optional
context: context:
- id: policy/security - name: repository-tree
kind: information-space
description: A listing of the repository under review.
requirement: required requirement: required
capabilities: capabilities:
- web-search - web-search
``` ```
### 10.1 Context dependencies
`context` names something the package needs that CPF neither packages nor
resolves: a live information space, an API, a corpus, a document the caller
supplies. Anything CPF *can* package is a package — a reusable policy or style
block belongs in `prompts` as a `type: fragment` package (§ 3.2), not here.
Entries use `name` rather than `id`, precisely because they are not package
identifiers and nothing can look them up.
| Field | Required | Meaning |
|---|---:|---|
| `name` | yes | What the context is called, in kebab-case |
| `description` | yes | What it is and what it must contain |
| `kind` | no | Free-form hint, e.g. `information-space`, `api`, `document`, `corpus` |
| `requirement` | no | `required` or `optional`; defaults to `required` |
`description` is required because nothing else can explain an unpackaged
dependency. A consumer meeting this entry has only the text to go on, and
INTENT principle 3 puts hidden context at odds with reuse.
A context dependency is not an input. An input is content the caller passes for
one use (§ 6); a context dependency is a standing fact about the environment
the package needs to be run in.
### 10.2 Capability dependencies
`capabilities` lists what the execution environment must be able to do, as
free-form lowercase kebab-case names — validated for shape, not for membership
in any vocabulary, exactly as `tags` are (§ 15). Standardizing model capability
vocabularies remains deferred; no real usage has yet asked for one.
A capability is not an artifact: it cannot be fetched, pinned or generated, so
it takes no version and no `requirement: generate`. It is either present or it
is not. A consumer that knows it lacks a required capability SHOULD say so
before running rather than after.
Where an environment knows the same capability by another name,
`compatibility.aliases` (§ 9) can record the correspondence.
Recommended requirement values: Recommended requirement values:
- `required` - `required`
@ -585,7 +664,7 @@ Recommended requirement values:
`generate` means that a resolver MAY satisfy a missing dependency by invoking an appropriate generation process. **CPF v0.1 does not define how generation or dependency resolution works.** `generate` means that a resolver MAY satisfy a missing dependency by invoking an appropriate generation process. **CPF v0.1 does not define how generation or dependency resolution works.**
### 10.1 Naming and pinning a dependency ### 10.3 Naming and pinning a dependency
A dependency's `id` MAY be a qualified `<registry>:<id>` reference (§ 3.2). A dependency's `id` MAY be a qualified `<registry>:<id>` reference (§ 3.2).
An unqualified id is resolved by the consumer against whatever registries it An unqualified id is resolved by the consumer against whatever registries it
@ -614,7 +693,7 @@ solver: each dependency is selected independently against what is available,
and no consumer is expected to satisfy constraints across a dependency graph. and no consumer is expected to satisfy constraints across a dependency graph.
A single lower bound is a selector, not a constraint system. A single lower bound is a selector, not a constraint system.
### 10.2 Composition ### 10.4 Composition
A package composes another in one of two ways, both expressed as an input A package composes another in one of two ways, both expressed as an input
default (§ 6.1) so that composition reuses the resolution machinery rather default (§ 6.1) so that composition reuses the resolution machinery rather
@ -736,7 +815,7 @@ Each entry is a single-key mapping, so a check may appear more than once.
`output.criteria` is a list of statements about a good result. Judging them `output.criteria` is a list of statements about a good result. Judging them
requires running the prompt and assessing what comes back, which CPF does not requires running the prompt and assessing what comes back, which CPF does not
specify and most consumers cannot do. They are **declared, not run** — the specify and most consumers cannot do. They are **declared, not run** — the
same division as `include` and `derive` in § 10.2, and for the same reason. same division as `include` and `derive` in § 10.4, and for the same reason.
#### Results are not part of the package #### Results are not part of the package
@ -838,13 +917,17 @@ A v0.1 validator SHOULD verify at least:
14. no default declares both `include` and `derive`; 14. no default declares both `include` and `derive`;
15. every dependency referenced by a composition or derived default declares a 15. every dependency referenced by a composition or derived default declares a
`version`, and that version is a semver literal, `any`, `newest`, or a `version`, and that version is a semver literal, `any`, `newest`, or a
`>=` lower bound (§ 10.1); `>=` lower bound (§ 10.3);
16. inclusion does not form a cycle; 16. inclusion does not form a cycle;
17. every eval file parses and declares a `schema`; 17. every eval file parses and declares a `schema`;
18. an eval declaring `canned-prompts/eval-rubric/v0.1` has a `name`, asserts 18. an eval declaring `canned-prompts/eval-rubric/v0.1` has a `name`, asserts
something via `render` or `output`, uses only known render checks, and — something via `render` or `output`, uses only known render checks, and —
when it declares an `example` — names a path listed in the manifest's when it declares an `example` — names a path listed in the manifest's
`examples`. `examples`;
19. every `dependencies.context` entry declares a `name` and a `description`,
and a `requirement` of `required` or `optional` if present;
20. capability names in `dependencies.capabilities` and
`compatibility.capabilities` are lowercase kebab-case.
A validator SHOULD additionally warn when a package intended for publication A validator SHOULD additionally warn when a package intended for publication
declares an inline derivation prompt (§ 6.1). declares an inline derivation prompt (§ 6.1).

View file

@ -140,6 +140,29 @@ local:practice/pqrst-estimate@0.2.0
An eval declares assessment, never results. Results are run evidence and live An eval declares assessment, never results. Results are run evidence and live
outside the immutable package. outside the immutable package.
## Required versus compatible
Two fields used to say overlapping things about capabilities. They now differ
in one word each:
| Field | Meaning | Absence means |
|-------|---------|---------------|
| `dependencies` | **required** — does not work without it | a consumer should warn or refuse |
| `compatibility` | **observed** — known to work with it | nothing; it is information |
Dependencies come in three kinds, separated by what the format can do about
them: `prompts` are packages it resolves by id and version, `context` names
things it does not package at all (an information space, an API, a corpus the
caller supplies), and `capabilities` are what the environment must be able to
do. `resolve` lists the last two, since the reference tool cannot verify
either:
```text
requires (this tool cannot verify these):
capability web-search
context repository-tree — A listing of the repository under review.
```
## Registries and identity ## Registries and identity
An id names a package *within a registry* (`CannedPromptFormat-v0.1.md` An id names a package *within a registry* (`CannedPromptFormat-v0.1.md`

View file

@ -21,7 +21,7 @@ inputs:
Inclusion is deterministic — the fragment's template is rendered and inlined as Inclusion is deterministic — the fragment's template is rendered and inlined as
text, with no model involved — so the reference CLI performs it during text, with no model involved — so the reference CLI performs it during
`resolve`. See `CannedPromptFormat-v0.1.md` § 10.2. `resolve`. See `CannedPromptFormat-v0.1.md` § 10.4.
Its `tone` parameter is inherited from the including package when that package Its `tone` parameter is inherited from the including package when that package
declares one, and otherwise falls back to the default here. declares one, and otherwise falls back to the default here.

View file

@ -39,7 +39,7 @@ output:
description: A 100% PQRST effort allocation with concise interpretation. description: A 100% PQRST effort allocation with concise interpretation.
compatibility: compatibility:
capabilities: capabilities:
- session-review - long-context
tags: tags:
- pqrst - pqrst
- retrospective - retrospective

View file

@ -76,6 +76,8 @@ rather than resolved by guessing.
`canned-prompts/eval-rubric/v0.1` schema, and reports output criteria as `canned-prompts/eval-rubric/v0.1` schema, and reports output criteria as
declared but not run. Unrecognized schemas are skipped, not rejected. A declared but not run. Unrecognized schemas are skipped, not rejected. A
failed render check exits non-zero. failed render check exits non-zero.
- `resolve` lists required capabilities and context dependencies, which this
tool cannot verify, rather than implying it checked them.
- An optional `registry.yaml` names a registry and records namespace claims. - An optional `registry.yaml` names a registry and records namespace claims.
`publish` warns when a namespace is declared `closed` — it cannot `publish` warns when a namespace is declared `closed` — it cannot
authenticate a publisher, and says so rather than implying it checked. authenticate a publisher, and says so rather than implying it checked.

View file

@ -25,6 +25,7 @@ LOCAL_REGISTRY = "local"
REGISTRY_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$") REGISTRY_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
EVAL_RUBRIC_SCHEMA = "canned-prompts/eval-rubric/v0.1" EVAL_RUBRIC_SCHEMA = "canned-prompts/eval-rubric/v0.1"
RENDER_CHECKS = ("contains", "not_contains", "resolves_all") RENDER_CHECKS = ("contains", "not_contains", "resolves_all")
CAPABILITY_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
PLACEHOLDER_RE = re.compile(r"{{\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*}}") PLACEHOLDER_RE = re.compile(r"{{\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*}}")
SEMVER_RE = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+].*)?$") SEMVER_RE = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+].*)?$")
REQUIRED_FIELDS = ("format", "id", "name", "version", "summary", "template") REQUIRED_FIELDS = ("format", "id", "name", "version", "summary", "template")
@ -137,6 +138,50 @@ def prompt_dependencies(manifest: dict[str, Any]) -> dict[str, dict[str, Any]]:
return declared return declared
def validate_capabilities(names: Any, where: str) -> list[str]:
"""Validation rule 20 of § 18: shape is checked, membership is not (§ 10.2)."""
if names is None:
return []
if not isinstance(names, list):
raise CannedPromptError(f"{where} must be a list")
for name in names:
if not isinstance(name, str) or not CAPABILITY_RE.match(name):
raise CannedPromptError(f"{where}: {name!r} must be lowercase kebab-case")
return list(names)
def validate_context_dependencies(manifest: dict[str, Any]) -> list[dict[str, Any]]:
"""Validation rule 19 of § 18.
A context dependency names something CPF does not package and cannot
resolve, so `description` is the only thing a consumer has to go on.
"""
dependencies = manifest.get("dependencies") or {}
entries = dependencies.get("context") or []
if not isinstance(entries, list):
raise CannedPromptError("dependencies.context must be a list")
for entry in entries:
if not isinstance(entry, dict):
raise CannedPromptError("each context dependency must be a mapping")
name = entry.get("name")
if not isinstance(name, str) or not CAPABILITY_RE.match(name):
raise CannedPromptError(
f"context dependency name {name!r} must be lowercase kebab-case; "
"context entries use 'name', not 'id', because nothing can look them up"
)
if not isinstance(entry.get("description"), str) or not entry["description"].strip():
raise CannedPromptError(
f"context dependency {name!r} must declare a description; nothing "
"else can explain a dependency the format does not package"
)
requirement = entry.get("requirement", "required")
if requirement not in ("required", "optional"):
raise CannedPromptError(
f"context dependency {name!r}: requirement must be 'required' or 'optional'"
)
return list(entries)
def read_eval(package_dir: Path, relative: str) -> dict[str, Any]: def read_eval(package_dir: Path, relative: str) -> dict[str, Any]:
path = safe_relative_file(package_dir, relative, "evals") path = safe_relative_file(package_dir, relative, "evals")
try: try:
@ -208,7 +253,7 @@ def check_composition_reference(
if "version" not in declared[reference]: if "version" not in declared[reference]:
raise CannedPromptError( raise CannedPromptError(
f"input {name!r} {field}s {reference!r}, so that dependency must " f"input {name!r} {field}s {reference!r}, so that dependency must "
"declare a version (§ 10.1)" "declare a version (§ 10.3)"
) )
@ -303,6 +348,16 @@ def validate_package(package_dir: Path) -> dict[str, Any]:
for item in manifest.get("inputs") or []: for item in manifest.get("inputs") or []:
validate_input_default(item, declared) validate_input_default(item, declared)
validate_context_dependencies(manifest)
validate_capabilities(
(manifest.get("dependencies") or {}).get("capabilities"),
"dependencies.capabilities",
)
validate_capabilities(
(manifest.get("compatibility") or {}).get("capabilities"),
"compatibility.capabilities",
)
declared_examples = [str(path) for path in (manifest.get("examples") or [])] declared_examples = [str(path) for path in (manifest.get("examples") or [])]
for relative in manifest.get("evals") or []: for relative in manifest.get("evals") or []:
validate_eval(relative, read_eval(package_dir, relative), declared_examples) validate_eval(relative, read_eval(package_dir, relative), declared_examples)
@ -402,7 +457,7 @@ def catalog_package_path(catalog: Path, registry: str, package_id: str, version:
def validate_version_selector(value: Any, where: str) -> str: def validate_version_selector(value: Any, where: str) -> str:
"""A semver literal, `any`, `newest`, or a `>=` lower bound (§ 10.1).""" """A semver literal, `any`, `newest`, or a `>=` lower bound (§ 10.3)."""
if not isinstance(value, str) or not value.strip(): if not isinstance(value, str) or not value.strip():
raise CannedPromptError(f"{where}: version must be a non-empty string") raise CannedPromptError(f"{where}: version must be a non-empty string")
selector = value.strip() selector = value.strip()
@ -422,7 +477,7 @@ def validate_version_selector(value: Any, where: str) -> str:
def select_version(available: list[str], selector: str | None) -> str | None: def select_version(available: list[str], selector: str | None) -> str | None:
"""Pick a version from `available` (newest first) per a § 10.1 selector. """Pick a version from `available` (newest first) per a § 10.3 selector.
Each dependency is selected independently against what is present. There is Each dependency is selected independently against what is present. There is
no constraint solving across a dependency graph; that stays a non-goal. no constraint solving across a dependency graph; that stays a non-goal.
@ -700,12 +755,12 @@ def resolve_inputs(
"""Resolve inputs and parameters per § 5.1. """Resolve inputs and parameters per § 5.1.
`composer`, when supplied, satisfies `include` defaults by rendering the `composer`, when supplied, satisfies `include` defaults by rendering the
included package (§ 10.2). Inclusion is deterministic, so a tool that can included package (§ 10.4). Inclusion is deterministic, so a tool that can
render can satisfy it; derivation is not, and this tool never calls a model, render can satisfy it; derivation is not, and this tool never calls a model,
so a derived default is satisfied only by its static fallback `value`. so a derived default is satisfied only by its static fallback `value`.
`inherited` carries the including package's already-resolved values into an `inherited` carries the including package's already-resolved values into an
included package (§ 10.2). Those values are used as-is rather than coerced, included package (§ 10.4). Those values are used as-is rather than coerced,
and do not count as caller-supplied. and do not count as caller-supplied.
""" """
result = Resolution() result = Resolution()
@ -729,7 +784,7 @@ def resolve_inputs(
result.underivable.append(name) result.underivable.append(name)
# Parameters resolve first: a composed input inherits the including # Parameters resolve first: a composed input inherits the including
# package's resolved values (§ 10.2), so those must already be settled. # package's resolved values (§ 10.4), so those must already be settled.
parameters = manifest.get("parameters") or {} parameters = manifest.get("parameters") or {}
for name, spec in parameters.items(): for name, spec in parameters.items():
known.add(name) known.add(name)
@ -794,7 +849,7 @@ def resolve_values(manifest: dict[str, Any], raw_values: dict[str, str]) -> dict
class CatalogComposer: class CatalogComposer:
"""Satisfies `include` defaults from the catalog (§ 10.2). """Satisfies `include` defaults from the catalog (§ 10.4).
Inclusion is text composition: the included package is rendered and its text Inclusion is text composition: the included package is rendered and its text
is inlined. No model is involved, so this is deterministic. is inlined. No model is involved, so this is deterministic.
@ -935,6 +990,21 @@ def cmd_resolve(args: argparse.Namespace) -> None:
print(f"{name:<{width}} {origin:<{origin_width}} {preview}") print(f"{name:<{width}} {origin:<{origin_width}} {preview}")
else: else:
print(f"{name:<{width}} {origin}") print(f"{name:<{width}} {origin}")
dependencies = manifest.get("dependencies") or {}
capabilities = dependencies.get("capabilities") or []
context = [
entry
for entry in (dependencies.get("context") or [])
if entry.get("requirement", "required") == "required"
]
if capabilities or context:
print()
print("requires (this tool cannot verify these):")
for name in capabilities:
print(f" capability {name}")
for entry in context:
print(f" context {entry['name']}{entry['description'].strip()}")
if resolution.underivable: if resolution.underivable:
print() print()
plural = len(resolution.underivable) > 1 plural = len(resolution.underivable) > 1

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.""" """A YAML fixture carries real types; only CLI strings need parsing."""
manifest = {"parameters": {"flag": {"type": "boolean", "default": False}}} manifest = {"parameters": {"flag": {"type": "boolean", "default": False}}}
assert cp.resolve_inputs(manifest, {"flag": True}).values["flag"] is True 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"]

View file

@ -222,10 +222,10 @@ Delivered:
1. § 3.2 defines `type` (`template` | `fragment`), closing the undefined-field 1. § 3.2 defines `type` (`template` | `fragment`), closing the undefined-field
gap. It is advisory: a fragment is a valid package and may be rendered gap. It is advisory: a fragment is a valid package and may be rendered
alone; the field records intent so a consumer can warn. alone; the field records intent so a consumer can warn.
2. § 10.1 (new) covers naming and pinning: qualified dependency ids, the four 2. § 10.3 (new) covers naming and pinning: qualified dependency ids, the four
version selectors, `version` required for anything composed, and an version selectors, `version` required for anything composed, and an
explicit statement of why this is not range resolution. explicit statement of why this is not range resolution.
3. § 10.2 (new) defines both composition kinds, parameter pass-through into an 3. § 10.4 (new) defines both composition kinds, parameter pass-through into an
included package, mandatory cycle detection, and the reasoned refusal of included package, mandatory cycle detection, and the reasoned refusal of
inheritance. inheritance.
4. § 6.1 gains the included-default form; § 5.1 gains resolution rule 6 for 4. § 6.1 gains the included-default form; § 5.1 gains resolution rule 6 for
@ -305,7 +305,7 @@ actually has.
```task ```task
id: CANP-WP-0002-T05 id: CANP-WP-0002-T05
status: todo status: done
priority: medium priority: medium
state_hub_task_id: "5beec7ba-e6c9-58b1-a065-b06aba015034" state_hub_task_id: "5beec7ba-e6c9-58b1-a065-b06aba015034"
``` ```
@ -316,10 +316,58 @@ manifest surface with no semantics whatsoever in v0.1, and § 9
Decide: what a context dependency declares, how it differs from an input, how Decide: what a context dependency declares, how it differs from an input, how
it relates to `compatibility.capabilities`, and whether capability names are it relates to `compatibility.capabilities`, and whether capability names are
free strings in v0.2 (model capability vocabularies stay deferred). T01 is now settled and partly answers this: a derived default is a free strings in v0.2 (model capability vocabularies stay deferred). **Decisions (operator, 2026-09-06):**
consumer-resolved context requirement expressed through `dependencies.prompts`
rather than through `dependencies.context`. Decide whether `context` is still a - *`context` names what CPF cannot package.* `prompts` names artifacts the
distinct concept or collapses into the prompt-dependency mechanism. format resolves by id and version; `context` names what it does not and will
not package — a live information space, an API, a corpus, a document the
caller supplies. Anything CPF *can* package is a package: a reusable policy
or style block belongs in `prompts` as a `type: fragment`.
- *Capability names are free strings*, lowercase kebab-case, validated for
shape and not for membership, exactly as `tags` are. Model capability
vocabularies stay deferred.
- *Terminology, not deduplication, for the capability overlap.* The operator's
answer was conditional — keep both fields if they carry the required/observed
distinction, and fix the terminology if they do not. **They did not.** § 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"). § 10 then described dependencies as what a prompt
"expects". Both fields said *expected*, so the ambiguity was real and the
fix was terminology.
Delivered:
1. § 9 rewritten: `compatibility` records **observations, never
requirements**, and a consumer MUST NOT refuse to run a package because its
environment is absent from those lists. Adds `aliases`, recording the same
capability under other names — the operator's point that a capability can
be "known by another name".
2. § 9.1 (new) states the distinction in one word each — `dependencies` means
*required*, `compatibility` means *observed* — with what absence implies
for each, and notes that the same name may legitimately appear in both.
3. § 10 opens with the three dependency kinds separated by what CPF can do
about them, and § 10.1 (new) specifies context dependencies: `name` rather
than `id` because nothing can look them up, a **required** `description`
because nothing else can explain an unpackaged dependency, optional `kind`,
and `requirement` limited to `required`/`optional`.
4. § 10.2 (new) specifies capability dependencies: free-form kebab-case, no
version and no `requirement: generate`, because a capability is not an
artifact and cannot be fetched, pinned or generated.
5. § 10.1 also draws the input/context line: an input is content passed for one
use; a context dependency is a standing fact about the environment.
6. § 18 gains validation rules 1920; § 4's manifest surface updated. Former
§ 10.1/10.2 renumbered to § 10.3/10.4 with all cross-references updated.
7. `reference/canned_prompts.py`: `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.
**Caught in review of my own change.** I first added a `session-record`
context dependency to `examples/pqrst-estimate` to demonstrate the feature,
then removed it: it described the `session_summary` *input*, which § 10.1
explicitly says a context dependency is not. The example now declares only
what it genuinely has. Illustrations live in the spec; examples stay honest.
## Rewrite specification section 23 ## Rewrite specification section 23