canned-prompts/reference/README.md
tegwick 95a8bb31d2 CANP-WP-0002 T07: semver precedence and strict packaging
Two defects from the original review of the seed.

Prerelease ordering was worse than first recorded. parse_semver returned
(major, minor, patch, raw_string), so 1.0.0-rc1 and 1.0.0 tied on the numeric
fields and then compared as strings — "1.0.0-rc1" > "1.0.0". A release
candidate therefore shadowed its own release for `newest` and for `>=`, not
just for the no-version case.

parse_semver now returns a SemVer section 11 precedence key: numeric fields, a
release/prerelease rank, then dot-separated prerelease identifiers with
numeric ones compared numerically. Build metadata is ignored. Beyond ordering,
prereleases are excluded from `any`, `newest` and `>=` entirely; only an exact
pin selects one, so publishing a release candidate never changes what existing
consumers resolve to. A package holding only prereleases now says so rather
than reporting a bare not-found.

Packaging copied the whole source directory, so a stray .git, virtualenv or
scratch file landed in the catalog and registry. Section 2 already required
otherwise — tools MUST ignore unknown non-reserved files unless a manifest
field references them — so this is conformance rather than a new rule. What is
new is that omissions are reported instead of silent:

  not packaged (not a reserved path, not referenced by the manifest): .git/, .venv/, notes.txt

LICENSE joins the reserved paths. Strict packaging would otherwise drop a
package's license text while faithfully copying its `license` field, which
contradicts section 14's instruction to surface licensing on publish and
install.

Spec: 2 (LICENSE, packaging obligation, reporting), 17.1 new, 10.3 note.

Reference CLI: parse_semver rewritten with is_prerelease; select_version and
pick_version updated; copy_package and report_skipped replace copy_immutable.
Tests 65 -> 78.

examples/pqrst-estimate carries a LICENSE and a license field, exercising the
new reserved path.

Also fixes a leaked loop variable in package_members that would have reported
a bad `template` path as an `evals` error.

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
2026-09-06 09:32:42 +02:00

3.6 KiB

canned-prompts reference CLI

This is intentionally a small reference implementation, not the intended final architecture.

It demonstrates eight verbs:

add      PATH
search   QUERY
show     ID
resolve  ID --set key=value
render   ID --set key=value
eval     ID
install  ID [--version VERSION]
publish  PATH

The implementation uses a local catalog plus a filesystem registry and performs no model calls.

resolve and render are separate because the specification separates them (§ 5.1): resolution decides each value and may be non-deterministic, rendering substitutes and always is. resolve prints where every value came from — supplied, default, or fallback — before any prompt is produced.

Stores

Default locations:

~/.canned-prompts/catalog
~/.canned-prompts/registry

A registry stores packages flat, because an id is unambiguous within one registry:

<registry>/<id path>/<version>/...

A catalog is namespaced by registry, because identity is registry-scoped (§ 3.2) and the same id may be installed from more than one place:

<catalog>/<registry name>/<id path>/<version>/...

For example:

~/.canned-prompts/catalog/house/practice/pqrst-estimate/0.1.0/
~/.canned-prompts/catalog/local/practice/pqrst-estimate/0.1.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 registry, so it files it under local (override with --as).

Commands that take an ID accept a bare id or a qualified <registry>:<id>. A bare id installed from more than one registry is reported as ambiguous rather than resolved by guessing.

Design choices

  • YAML manifest via PyYAML.
  • {{ name }} template substitution only.
  • No arbitrary expression/code execution.
  • Published versions are immutable by default.
  • add, publish and install copy only reserved paths and manifest- referenced files. A working directory's .git, virtualenv or scratch files never ship; whatever is left out is named on stderr.
  • Version precedence follows SemVer: a prerelease ranks below its release, and any, newest and >= skip prereleases entirely. Only an exact pin selects one.
  • install copies from registry to catalog.
  • add copies a package directly to catalog.
  • search, show, resolve, and render operate on catalog packages, and print qualified <registry>:<id> references.
  • include defaults are satisfied (inclusion is deterministic); derive defaults are reported, not run. Inclusion cycles are detected and named.
  • eval runs the deterministic render checks of any eval declaring the canned-prompts/eval-rubric/v0.1 schema, and reports output criteria as declared but not run. Unrecognized schemas are skipped, not rejected. A 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. publish warns when a namespace is declared closed — it cannot authenticate a publisher, and says so rather than implying it checked.
  • Static input defaults are applied; derived defaults (§ 6.1) are not. This tool never calls a model, so a derived default is satisfied only by its static fallback value. Without one, resolve reports the input as unresolved and render refuses rather than substituting empty text.

Use this implementation to challenge the format. Replace it once real usage reveals the right architecture.