canned-prompts/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

202 lines
7.6 KiB
Markdown

# canned-prompts seed
> **Collect, reuse and share prompts and prompt templates.**
This bundle contains a first project seed for `canned-prompts`:
- [`INTENT.md`](INTENT.md) — project mission, boundaries, principles, and success criteria.
- [`CannedPromptFormat-v0.1.md`](CannedPromptFormat-v0.1.md) — experimental package-format specification.
- [`reference/`](reference/) — deliberately small Python CLI implementing the basic lifecycle.
- [`examples/pqrst-estimate/`](examples/pqrst-estimate/) — a real package that can be used to exercise the implementation.
- [`examples/house-style/`](examples/house-style/) — a `type: fragment` package that `pqrst-estimate` composes.
## Try the reference implementation
```bash
cd reference
python -m venv .venv
. .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt
# add the included examples to your local catalog
python canned_prompts.py add ../examples/house-style
python canned_prompts.py add ../examples/pqrst-estimate
# find and inspect it
python canned_prompts.py search pqrst
python canned_prompts.py show practice/pqrst-estimate
# see how each input and parameter resolves, and where the value came from
python canned_prompts.py resolve practice/pqrst-estimate \
--set session_summary="Implemented feature X, read unfamiliar code, added tests."
# render it
python canned_prompts.py render practice/pqrst-estimate \
--set session_summary="Implemented feature X, read unfamiliar code, added tests."
# publish it to the local filesystem registry
python canned_prompts.py publish ../examples/pqrst-estimate
# remove the local catalog if you want to simulate another machine, then install
python canned_prompts.py install practice/pqrst-estimate --version 0.1.0
```
Packages added from a path are filed under the registry name `local`; packages
installed from a registry are filed under that registry's name. `search` prints
qualified references:
```text
house:practice/pqrst-estimate@0.1.0 PQRST Estimate
local:practice/pqrst-estimate@0.1.0 PQRST Estimate
```
By default the reference tool uses:
```text
~/.canned-prompts/catalog
~/.canned-prompts/registry
```
Override them with:
```text
CANNED_PROMPTS_HOME=/some/path
```
or command-level `--catalog` / `--registry` options.
## Resolution vs rendering
The format separates the two steps (`CannedPromptFormat-v0.1.md` § 5.1).
Resolution decides a value for every input and parameter and may be
non-deterministic; rendering substitutes those values and always is. An input
may declare a default that is either a static value or a *derived* one — a
prompt that a capable consumer may run to produce the value, declared without
naming any resolver or model.
This reference tool never calls a model, so it resolves supplied values and
static defaults only, and reports anything it cannot derive instead of
rendering a prompt with a silent hole in it.
## Composition
A package composes another by declaring it as a dependency and binding it to an
input default. There are two kinds:
- **`include`** — inline the other package's *rendered template* as text.
Deterministic, needs no model, and the reference CLI performs it.
- **`derive`** — use the other package's *result*, obtained by running it.
Only a consumer able to run it can supply one.
`examples/pqrst-estimate` includes `examples/house-style`, so a shared style
block is a versioned package rather than copied text:
```yaml
dependencies:
prompts:
- id: practice/house-style
version: ">= 0.1.0"
requirement: required
inputs:
- name: house_style
required: false
default:
include: practice/house-style
```
A dependency pins an exact version by default; `any`, `newest` and a `>= X.Y.Z`
lower bound are explicit opt-ins. These are per-dependency selectors, not
version ranges — there is no solver, and constraint resolution across a
dependency graph remains a non-goal.
There is no template inheritance. A package never extends another or overrides
its parts; composition is by reference only, so a package's content stays
readable without chasing ancestors.
## Evals
An eval file declares what to assess. `evals/` used to hold whatever an author
put there; it now has one schema the tooling understands, split along the same
line as composition:
- **render checks** — deterministic assertions about the *rendered prompt*
(`contains`, `not_contains`, `resolves_all`). No model needed, so the
reference CLI runs them.
- **output criteria** — statements about a good *result*. Declared, not run.
```bash
python canned_prompts.py eval practice/pqrst-estimate
```
```text
local:practice/pqrst-estimate@0.2.0
evals/quality.yaml (pqrst-estimate-quality)
render PASS contains "must sum to exactly 100%"
render PASS resolves_all
output -- 4 criteria declared (not run: judging output needs a model)
```
An eval declares assessment, never results. Results are run evidence and live
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
An id names a package *within a registry* (`CannedPromptFormat-v0.1.md`
§ 3.2). The same id obtained from two registries may be two different
packages, so the catalog keeps them apart and a bare id that matches more than
one is reported as ambiguous rather than guessed. Qualify it when you need to:
```bash
python canned_prompts.py render house:practice/pqrst-estimate --set ...
```
A registry may describe itself with an optional `registry.yaml` naming it and
recording which namespaces are claimed and under what policy. Those claims are
descriptive: a filesystem registry cannot authenticate a publisher, and
signing and trust scoring are explicit non-goals. Ownership lives with the
registry rather than in the package, so no package carries an unverifiable
assertion of authority.
## Packaging and versions
`add`, `publish` and `install` copy the reserved paths (`prompt.yaml`,
`prompt.md`, `README.md`, `LICENSE`, `examples/`, `evals/`, `assets/`) plus
anything a manifest field references — and nothing else, as § 2 requires. What
was left behind is reported rather than silently dropped:
```text
not packaged (not a reserved path, not referenced by the manifest): .git/, .venv/, notes.txt
```
Version precedence follows SemVer, so `1.0.0` outranks `1.0.0-rc1`, and
`any`, `newest` and `>= X.Y.Z` skip prereleases entirely. Publishing a release
candidate never changes what existing consumers resolve to; name it exactly to
use it.
## Deliberate limitations
This seed has no hosted registry, model execution, authentication, network access, dependency resolver, or social features. `publish` and `install` operate on a filesystem registry so that the package semantics can be tested before infrastructure is built around them.