Report unsatisfied dependencies on add and install

Found while verifying the repo against INTENT.md's eight success criteria for
a first practical release, after CANP-WP-0002 finished.

Criterion 5 is "publish a version to a registry and install it elsewhere".
Installing practice/pqrst-estimate into a fresh catalog reported success, and
rendering it then failed with "included package not found:
practice/house-style". CANP-WP-0002-T03 introduced this — composition made a
package installable but unrenderable, and nothing said so at install time.

The error was honest, but reaching it after a successful-looking install is
the silently-incomplete failure section 23 now names as the thing this format
avoids.

`add` and `install` now name declared prompt dependencies the target catalog
cannot satisfy, including a package present at no matching version. They do
not fetch anything: section 10 leaves dependency resolution to the consumer,
and auto-installing transitively is a resolver — a separate decision from
refusing to hand over a package that looks fine and is not.

Whether `install` should offer `--with-dependencies` is left open as a design
question rather than smuggled in as a bug fix.

Tests 81 -> 84. Recorded as CANP-WP-ADHOC-2026-09-06.

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 14:48:08 +02:00
parent 526e9abdfd
commit 4b2bda0013
5 changed files with 130 additions and 0 deletions

View file

@ -181,6 +181,20 @@ signing and trust scoring are explicit non-goals. Ownership lives with the
registry rather than in the package, so no package carries an unverifiable registry rather than in the package, so no package carries an unverifiable
assertion of authority. assertion of authority.
## Dependencies are reported, not fetched
Installing a package that composes another warns when the dependency is
absent, including when it is present but no version matches:
```text
declared dependencies not in this catalog: practice/house-style@>= 0.1.0
— install them, or composition referencing them will not resolve
```
Nothing is fetched automatically. Dependency resolution is left to the
consumer, but a package that installs cleanly and then cannot render is worse
than one that says what it is waiting for.
## Packaging and versions ## Packaging and versions
`add`, `publish` and `install` copy the reserved paths (`prompt.yaml`, `add`, `publish` and `install` copy the reserved paths (`prompt.yaml`,

View file

@ -84,6 +84,9 @@ rather than resolved by guessing.
failed render check exits non-zero. failed render check exits non-zero.
- `resolve` lists required capabilities and context dependencies, which this - `resolve` lists required capabilities and context dependencies, which this
tool cannot verify, rather than implying it checked them. tool cannot verify, rather than implying it checked them.
- `add` and `install` name declared prompt dependencies the catalog cannot
satisfy. They do not fetch them — resolution is the consumer's job (§ 10) —
but a package that looks installed and cannot render should say so.
- 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

@ -678,6 +678,38 @@ def copy_package(src: Path, dst: Path, manifest: dict[str, Any], what: str) -> l
return skipped return skipped
def missing_dependencies(catalog: Path, manifest: dict[str, Any]) -> list[str]:
"""Declared prompt dependencies that this catalog cannot satisfy.
CPF leaves dependency *resolution* to the consumer (§ 10), so this tool does
not fetch anything. It does refuse to hand over a package that looks
installed but cannot render.
"""
missing: list[str] = []
for reference, entry in prompt_dependencies(manifest).items():
registry, package_id = parse_reference(reference)
names = [registry] if registry is not None else catalog_registries(catalog)
selector = entry.get("version")
if any(
select_version(versions_at(id_path(catalog / name, package_id)), selector)
for name in names
):
continue
missing.append(f"{reference}@{selector}" if selector else reference)
return missing
def report_missing_dependencies(catalog: Path, manifest: dict[str, Any]) -> None:
missing = missing_dependencies(catalog, manifest)
if missing:
print(
"declared dependencies not in this catalog: "
+ ", ".join(missing)
+ " — install them, or composition referencing them will not resolve",
file=sys.stderr,
)
def report_skipped(skipped: list[str]) -> None: def report_skipped(skipped: list[str]) -> None:
if skipped: if skipped:
print( print(
@ -705,6 +737,7 @@ def cmd_add(args: argparse.Namespace) -> None:
registry = check_registry_name(args.as_registry) registry = check_registry_name(args.as_registry)
dst = catalog_package_path(catalog, registry, manifest["id"], manifest["version"]) dst = catalog_package_path(catalog, registry, manifest["id"], manifest["version"])
report_skipped(copy_package(src.resolve(), dst, manifest, "catalog package")) report_skipped(copy_package(src.resolve(), dst, manifest, "catalog package"))
report_missing_dependencies(catalog, manifest)
print(f"added {registry}:{manifest['id']}@{manifest['version']} -> {dst}") print(f"added {registry}:{manifest['id']}@{manifest['version']} -> {dst}")
@ -745,6 +778,7 @@ def cmd_install(args: argparse.Namespace) -> None:
manifest = validate_package(src) manifest = validate_package(src)
dst = catalog_package_path(catalog, name, manifest["id"], manifest["version"]) dst = catalog_package_path(catalog, name, manifest["id"], manifest["version"])
copy_package(src, dst, manifest, "catalog package") copy_package(src, dst, manifest, "catalog package")
report_missing_dependencies(catalog, manifest)
print(f"installed {name}:{manifest['id']}@{manifest['version']} -> {dst}") print(f"installed {name}:{manifest['id']}@{manifest['version']} -> {dst}")

View file

@ -785,3 +785,26 @@ def test_unknown_format_is_rejected(tmp_path: Path) -> None:
) )
with pytest.raises(cp.CannedPromptError, match="unsupported format"): with pytest.raises(cp.CannedPromptError, match="unsupported format"):
cp.validate_package(pkg) cp.validate_package(pkg)
# --- unsatisfied dependencies are reported, not fetched (§ 10) ---
def test_missing_dependency_is_reported(composed: Path) -> None:
"""Installing a composing package without its fragment must not look fine."""
package_dir, _ = cp.resolve_installed(composed, "review/change", None)
manifest = cp.validate_package(package_dir)
empty = composed.parent / "empty-catalog"
assert cp.missing_dependencies(empty, manifest) == ["style/house@1.0.0"]
def test_present_dependency_is_not_reported(composed: Path) -> None:
package_dir, _ = cp.resolve_installed(composed, "review/change", None)
manifest = cp.validate_package(package_dir)
assert cp.missing_dependencies(composed, manifest) == []
def test_dependency_present_but_wrong_version_is_reported(composed: Path) -> None:
package_dir, _ = cp.resolve_installed(composed, "review/change", None)
manifest = cp.validate_package(package_dir)
manifest["dependencies"]["prompts"][0]["version"] = "9.9.9"
assert cp.missing_dependencies(composed, manifest) == ["style/house@9.9.9"]

View file

@ -0,0 +1,56 @@
---
id: CANP-WP-ADHOC-2026-09-06
type: workplan
title: "Ad hoc fixes — 2026-09-06"
domain: agents
repo: canned-prompts
status: finished
owner: codex
topic_slug: practice
created: "2026-09-06"
updated: "2026-09-06"
---
# Ad hoc fixes — 2026-09-06
Low-risk fixes found while verifying the repo against `INTENT.md`'s eight
success criteria for a first practical release, after `CANP-WP-0002` finished.
## Report unsatisfied dependencies on add and install
```task
id: CANP-WP-ADHOC-2026-09-06-T01
status: done
priority: medium
```
**Found by verification.** INTENT success criterion 5 is "publish a version to
a registry and install it elsewhere". Installing `practice/pqrst-estimate` into
a fresh catalog reported success, and rendering it then failed with
`included package not found: practice/house-style`. `CANP-WP-0002-T03`
introduced this: composition made a package installable but unrenderable, and
nothing said so at install time.
The error itself was honest, but reaching it *after* a successful-looking
install is the "silently incomplete" failure that § 23 now names as the thing
this format avoids.
`add` and `install` now check declared prompt dependencies against the target
catalog and name any the catalog cannot satisfy, including the case where the
package is present but no version matches the selector:
```text
declared dependencies not in this catalog: practice/house-style@>= 0.1.0
— install them, or composition referencing them will not resolve
```
They do **not** fetch anything. § 10 leaves dependency resolution to the
consumer, and auto-installing transitively is a resolver, which is a different
decision from refusing to hand over a package that looks fine and is not.
`missing_dependencies` and `report_missing_dependencies` in
`reference/canned_prompts.py`; tests 81 → 84.
**Left open deliberately:** whether `install` should offer to fetch
dependencies (`--with-dependencies`) is a real design question, not a bug fix.
It wants its own decision rather than being smuggled in here.