Section 10.4 said nothing about inclusion being textual and repeated, so an author met the behaviour only by reading rendered output carefully. That silence was the defect. Writing it up got the mechanism wrong at first. I documented a dependency "reached by two paths" through two include chains, and wrote a detector that counted composer calls to match. Tested it against the real case that motivated the task — helix/repo-advance — and it did not fire, though the conventions block still rendered twice. The actual mechanism is subtler. helix/commit-sync declares its own `conventions` input; when composed, resolution passes the including package's already-resolved values down by name, so it *inherits* the outer text rather than resolving its own include, and renders it again. Nothing was included twice; the text appeared twice regardless. Both mechanisms are now documented with worked diagrams. duplicate_inclusions detects both — included values that are equal, and an included value contained within another — and resolve, render and eval warn. Nothing is deduplicated, as leaned. Deduplicating means choosing which occurrence survives, since position in a prompt carries meaning, and deciding what happens when two paths select different versions of the same dependency, which section 10.3 permits. That is resolver behaviour and section 10.4 keeps composition declarative. Verified by reintroducing the diamond in a scratch copy of repo-advance, which warns, and confirming the shipped factored collection stays silent. Tests 90 -> 95. 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
107 lines
4.3 KiB
Markdown
107 lines
4.3 KiB
Markdown
# canned-prompts reference CLI
|
|
|
|
This is intentionally a **small reference implementation**, not the intended final architecture.
|
|
|
|
It demonstrates nine verbs:
|
|
|
|
```text
|
|
add PATH
|
|
search QUERY
|
|
show ID
|
|
resolve ID --set key=value
|
|
render ID --set key=value
|
|
eval ID
|
|
index [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:
|
|
|
|
```text
|
|
~/.canned-prompts/catalog
|
|
~/.canned-prompts/registry
|
|
```
|
|
|
|
A **registry** stores packages flat, because an id is unambiguous within one
|
|
registry:
|
|
|
|
```text
|
|
<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:
|
|
|
|
```text
|
|
<catalog>/<registry name>/<id path>/<version>/...
|
|
```
|
|
|
|
For example:
|
|
|
|
```text
|
|
~/.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.
|
|
- `resolve`, `render` and `eval` warn when an included value will render more
|
|
than once — two inputs including the same thing, or an included package
|
|
inheriting an outer input of the same name. Nothing is deduplicated; the fix
|
|
is a better factoring.
|
|
- `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.
|
|
- `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.
|
|
- `add`, `install` and `publish` record an entry in the store's `index.yaml`:
|
|
source, method, first-inclusion date, and the package's declared author,
|
|
source and licence. `index` lists it. Re-adding keeps the original
|
|
`included_at` and updates `last_seen_at`.
|
|
- 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.
|