diff --git a/README.md b/README.md index 64a17f3..de47959 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ By default the reference tool uses: ```text ~/.canned-prompts/catalog -~/.canned-prompts/registry +~/.canned-prompts/default # the default registry ``` Override them with: diff --git a/reference/README.md b/reference/README.md index 4632366..c7a2abe 100644 --- a/reference/README.md +++ b/reference/README.md @@ -29,7 +29,7 @@ Default locations: ```text ~/.canned-prompts/catalog -~/.canned-prompts/registry +~/.canned-prompts/default ``` A **registry** stores packages flat, because an id is unambiguous within one @@ -49,12 +49,16 @@ A **catalog** is namespaced by registry, because identity is registry-scoped For example: ```text -~/.canned-prompts/catalog/house/practice/pqrst-estimate/0.1.0/ -~/.canned-prompts/catalog/local/practice/pqrst-estimate/0.1.0/ +~/.canned-prompts/catalog/default/practice/pqrst-estimate/1.0.0/ +~/.canned-prompts/catalog/local/practice/pqrst-estimate/1.0.0/ ``` A registry's name comes from its optional `registry.yaml`, and otherwise from -its directory basename. `add` takes a package from a path rather than a +its directory basename — which is why the default registry directory is called +`default` and not `registry`: the basename reaches qualified references, +catalog paths and index rows, and `registry:practice/thing` reads poorly. A +store left at the old `~/.canned-prompts/registry` is reported with the command +to move it, rather than a fresh empty one being created beside it. `add` takes a package from a path rather than a registry, so it files it under `local` (override with `--as`). Commands that take an ID accept a bare id or a qualified `:`. diff --git a/reference/canned_prompts.py b/reference/canned_prompts.py index 953daf4..fd9ffc9 100755 --- a/reference/canned_prompts.py +++ b/reference/canned_prompts.py @@ -55,9 +55,30 @@ def default_catalog() -> Path: def default_registry() -> Path: + # Named "default" rather than "registry" because § 20.1 takes a registry's + # name from its directory basename, and that name reaches qualified + # references, catalog paths and index rows. + return home_dir() / "default" + + +def legacy_registry() -> Path: return home_dir() / "registry" +def check_legacy_registry(registry: Path) -> None: + """Refuse to silently start a fresh default store beside a populated one.""" + if registry.resolve() != default_registry().resolve(): + return + if registry.exists() or not legacy_registry().is_dir(): + return + raise CannedPromptError( + f"the default registry moved from {legacy_registry()} to {registry}, so " + f"a registry's basename names it (§ 20.1) rather than reading as " + f"'registry:'. Move it:\n mv {legacy_registry()} {registry}\n" + f"or keep using the old one explicitly with --registry {legacy_registry()}" + ) + + def read_manifest(package_dir: Path) -> dict[str, Any]: manifest_path = package_dir / "prompt.yaml" if not manifest_path.is_file(): @@ -858,6 +879,7 @@ def cmd_publish(args: argparse.Namespace) -> None: src = Path(args.path) manifest = validate_package(src) registry = Path(args.registry).expanduser() + check_legacy_registry(registry) name = registry_name(registry) if registry.is_dir() else None policy, claim = namespace_policy(registry, manifest["id"]) if registry.is_dir() else ("open", None) @@ -879,6 +901,7 @@ def cmd_publish(args: argparse.Namespace) -> None: def cmd_install(args: argparse.Namespace) -> None: registry = Path(args.registry).expanduser() + check_legacy_registry(registry) catalog = Path(args.catalog).expanduser() name = registry_name(registry) diff --git a/reference/tests/test_canned_prompts.py b/reference/tests/test_canned_prompts.py index f96d599..c2abe58 100644 --- a/reference/tests/test_canned_prompts.py +++ b/reference/tests/test_canned_prompts.py @@ -914,3 +914,31 @@ def test_empty_inclusion_is_not_flagged() -> None: origins={"a": "included from x/empty", "b": "included from x/b"}, ) assert cp.duplicate_inclusions(resolution) == [] + + +# --- default registry naming (CANP-WP-0003) --- + +def test_default_registry_is_named_default(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setenv("CANNED_PROMPTS_HOME", str(tmp_path)) + assert cp.default_registry().name == "default" + + +def test_legacy_registry_is_reported_not_ignored(tmp_path: Path, monkeypatch) -> None: + """A fresh empty store beside a populated one would be worse than the wart.""" + monkeypatch.setenv("CANNED_PROMPTS_HOME", str(tmp_path)) + (tmp_path / "registry").mkdir() + with pytest.raises(cp.CannedPromptError, match="default registry moved"): + cp.check_legacy_registry(cp.default_registry()) + + +def test_no_complaint_once_migrated(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setenv("CANNED_PROMPTS_HOME", str(tmp_path)) + (tmp_path / "default").mkdir() + (tmp_path / "registry").mkdir() + cp.check_legacy_registry(cp.default_registry()) + + +def test_explicit_registry_path_is_never_second_guessed(tmp_path: Path, monkeypatch) -> None: + monkeypatch.setenv("CANNED_PROMPTS_HOME", str(tmp_path)) + (tmp_path / "registry").mkdir() + cp.check_legacy_registry(tmp_path / "somewhere-else") diff --git a/workplans/CANP-WP-0003-registry-naming-residual.md b/workplans/CANP-WP-0003-registry-naming-residual.md index 8c5e2a4..96dbe47 100644 --- a/workplans/CANP-WP-0003-registry-naming-residual.md +++ b/workplans/CANP-WP-0003-registry-naming-residual.md @@ -4,7 +4,7 @@ type: workplan title: "Default registry name reads as 'registry:'" domain: agents repo: canned-prompts -status: proposed +status: finished owner: codex topic_slug: practice created: "2026-09-06" @@ -20,7 +20,7 @@ Residual from `CANP-WP-0002` (`origin: residual`, `origin_ref: CANP-WP-0002`). ```task id: CANP-WP-0003-T01 -status: todo +status: done priority: low state_hub_task_id: "ead2d409-2b80-5f50-9f0f-a397822ba2ae" ``` @@ -54,3 +54,28 @@ Options worth weighing: `registry.yaml` — already exists and is documented. Option 3 is the current state. Pick deliberately rather than by default. + +## Outcome + +**Decision (operator, 2026-09-06): option 2** — rename the default directory to +`~/.canned-prompts/default`, so the § 20.1 basename rule produces a good name +with no special case, no file written into the user's store, and no second +naming mechanism. + +**The cost of deferring had been understated.** The workplan recorded this as +reading poorly in one output line. It is not only display: the name reaches +qualified references, the on-disk catalog layout (`catalog/registry/...`) and +`index.yaml` rows (`registry: registry`). Once packages are installed and +inclusions recorded, changing it is a migration rather than an edit — which is +an argument for deciding early, not late. + +Delivered: + +- `default_registry()` returns `~/.canned-prompts/default`. +- `check_legacy_registry` refuses to silently create a fresh empty store beside + a populated `~/.canned-prompts/registry`. It names the exact `mv`, and the + `--registry` flag for keeping the old one. Silently starting empty would have + been worse than the wart being fixed. +- The check fires only for the default path; an explicitly passed `--registry` + is never second-guessed. +- Docs updated in both READMEs. Tests 95 → 99.