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

@ -84,6 +84,9 @@ rather than resolved by guessing.
failed render check exits non-zero.
- `resolve` lists required capabilities and context dependencies, which this
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.
`publish` warns when a namespace is declared `closed` — it cannot
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
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:
if skipped:
print(
@ -705,6 +737,7 @@ def cmd_add(args: argparse.Namespace) -> None:
registry = check_registry_name(args.as_registry)
dst = catalog_package_path(catalog, registry, manifest["id"], manifest["version"])
report_skipped(copy_package(src.resolve(), dst, manifest, "catalog package"))
report_missing_dependencies(catalog, manifest)
print(f"added {registry}:{manifest['id']}@{manifest['version']} -> {dst}")
@ -745,6 +778,7 @@ def cmd_install(args: argparse.Namespace) -> None:
manifest = validate_package(src)
dst = catalog_package_path(catalog, name, manifest["id"], manifest["version"])
copy_package(src, dst, manifest, "catalog package")
report_missing_dependencies(catalog, manifest)
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"):
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"]