CANP-WP-0002 T03: composition by reference, two kinds

T01 had already delivered half of composition without naming it: a derived
default binds an input to a prompt dependency, which is transclusion — run
package B, use its output. What was missing was the deterministic half.

`include` inlines another package's rendered template as text. No model is
involved, so the reference CLI can actually perform it, and a shared preamble,
rubric or style block becomes a versioned package instead of copied text. This
is the concrete way to honor INTENT principle 9 without any runtime. `derive`
stays as it was. Both are input defaults, so composition reuses the resolution
machinery rather than adding a second one.

No template inheritance. Four of this repo's own documents argue against it:
INTENT principle 3 (hidden context defeats reuse), section 19's "make package
contents visible before execution", section 17's requirement that behavior
changes produce a new version, and the non-goal on range resolution.

Version selectors: an exact pin is the expected form, with `any`, `newest` and
`>= X.Y.Z` as explicit opt-ins so looseness is written rather than implied by
absence. Selectors are evaluated per dependency against what is available —
no solver, no cross-dependency constraint satisfaction — which is what keeps
them outside the range-resolution non-goal, and the spec says so.

Also defines `type` (template | fragment), which appeared once in the section 4
manifest surface and was specified nowhere.

Spec: 3.2 (type), 5.1 (inclusion resolution rule, renumbered), 6.1 (included
default), 10.1 and 10.2 (new), 18 (rules 14-16), 21.

Reference CLI: validate_version_selector, select_version,
prompt_dependencies replacing prompt_dependency_ids,
check_composition_reference, CatalogComposer with cycle detection, and
resolve_inputs gaining composer= and inherited=. Tests 21 -> 42.

Examples: house-style is a real fragment package; pqrst-estimate composes it
and is bumped 0.1.0 -> 0.2.0 per section 17.

Fixes an ordering bug found while testing: inputs resolved before parameters,
so an included package could not see the including package's parameters and
silently fell back to its own defaults — the fragment rendered tone=neutral
where the including package said blunt. Parameters now resolve first; the
report still lists inputs first.

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 01:31:58 +02:00
parent 352ed5b30a
commit 4a56f20209
11 changed files with 697 additions and 58 deletions

View file

@ -173,6 +173,21 @@ A short description of the intended purpose. A consumer SHOULD be able to decide
Relative path to the primary prompt template inside the package. The path MUST remain within the package directory.
#### `type`
Declares what kind of artifact the package is. Optional; defaults to
`template`.
| Value | Meaning |
|---|---|
| `template` | A complete prompt, intended to be used on its own |
| `fragment` | A reusable block intended for inclusion in other packages (§ 10.1) |
`type` is advisory. A fragment is a perfectly valid package and MAY be
rendered on its own; the field records the author's intent so that a consumer
can warn when a package is used against it — rendering a fragment as a
standalone prompt, or including a whole template where a fragment was meant.
## 4. Complete v0.1 manifest surface
```yaml
@ -297,22 +312,27 @@ always produce the same output. A consumer that satisfies a derived default
permitted to derive it. A consumer that does not derive uses the default's
static fallback `value` when one is declared, and otherwise leaves the
input unresolved.
6. Resolution MUST report which values were derived, so that a caller can see
what was added on its behalf before the prompt is used.
6. An included input default is satisfied by rendering the included package
(§ 10.2). This is deterministic, so a consumer that can render can satisfy
it; one that cannot locate the package uses the static fallback `value`
when declared, and otherwise leaves the input unresolved.
7. Resolution MUST report which values were derived or included, so that a
caller can see what was added on its behalf before the prompt is used.
**Rendering rules:**
7. Values are substituted as text in v0.1.
8. A placeholder with no resolved value is an error.
9. Template evaluation MUST NOT execute arbitrary code.
10. Rendering MUST NOT derive values. A tool offering derivation MUST perform
8. Values are substituted as text in v0.1.
9. A placeholder with no resolved value is an error.
10. Template evaluation MUST NOT execute arbitrary code.
11. Rendering MUST NOT derive values. A tool offering derivation MUST perform
it as a distinct resolve step whose results are visible to the caller
before rendering.
before rendering. Inclusion likewise happens during resolution; rendering
only substitutes.
A minimal implementation may implement resolution for supplied values and
static defaults only. Such a tool is conforming: it reports an input with an
unsatisfied derived default and no static fallback as unresolved, which
rule 8 makes an error.
rule 9 makes an error.
CPF v0.1 does not define conditionals, loops, filters, or functions. Implementations MAY offer richer rendering modes only when explicitly declared by an extension; they MUST NOT silently reinterpret a v0.1 template as executable code.
@ -426,10 +446,41 @@ the situation this format exists to replace. Validators SHOULD warn when a
**published** package derives inline. Inline derivation is intended for local
and draft packages.
`value` inside a derived default is an optional **static fallback**. Its
#### Included default
A default may instead **include** another package's rendered template as text
(§ 10.2). Unlike a derived default this is deterministic and needs no model,
so every implementation that can render can also include:
```yaml
dependencies:
prompts:
- id: style/house
version: 1.0.0
requirement: required
inputs:
- name: house_style
type: content
required: false
default:
include: style/house
```
`include` names a package declared in `dependencies.prompts`, exactly as a
`derive` reference does. A single default MUST declare at most one of
`include` or `derive`.
An included default MAY also declare a static fallback `value`, used by a
consumer that cannot locate the included package.
#### Static fallback
`value` inside a derived or included default is an optional **static fallback**. Its
meaning is defined by resolution rule 5: a consumer that does not derive uses
it, and an input with neither a derived value nor a fallback stays unresolved,
which rule 8 makes an error. Derivation never silently yields empty content.
The same holds for an inclusion that cannot be resolved.
Declaring a derived default does not oblige any consumer to derive anything,
and does not make the package depend on a particular resolver, model or
@ -534,6 +585,68 @@ Recommended requirement values:
`generate` means that a resolver MAY satisfy a missing dependency by invoking an appropriate generation process. **CPF v0.1 does not define how generation or dependency resolution works.**
### 10.1 Naming and pinning a dependency
A dependency's `id` MAY be a qualified `<registry>:<id>` reference (§ 3.2).
An unqualified id is resolved by the consumer against whatever registries it
draws on, and an id matching packages from more than one registry MUST be
reported as ambiguous rather than chosen.
`version` selects which version satisfies the dependency:
| `version` | Selects |
|---|---|
| a semver literal, e.g. `1.2.0` | exactly that version |
| `any` | any available version; a consumer SHOULD prefer one it already holds |
| `newest` | the newest available version |
| `>= 1.2.0` | the newest available version that is at least `1.2.0` |
**An exact pin is the expected form.** The other three are explicit opt-ins,
visible in the manifest, so that looseness is always something an author wrote
rather than something absence implied.
`version` is REQUIRED for any dependency referenced by a composition or a
derived default (§ 6.1), and RECOMMENDED otherwise.
This is deliberately **not** semantic version range resolution, which remains
a non-goal. There are no unions, no caret or tilde operators, and above all no
solver: each dependency is selected independently against what is available,
and no consumer is expected to satisfy constraints across a dependency graph.
A single lower bound is a selector, not a constraint system.
### 10.2 Composition
A package composes another in one of two ways, both expressed as an input
default (§ 6.1) so that composition reuses the resolution machinery rather
than adding a second one:
| Form | Produces | Deterministic |
|---|---|---|
| `include` | the other package's **rendered template**, inlined as text | yes |
| `derive` | the other package's **result**, obtained by running it | no |
`include` is text composition: a shared preamble, rubric or house-style block
becomes a real versioned package instead of copied text, and inlining it needs
no model. `derive` is output composition: the value is whatever running the
other package produces, and only a consumer able to run it can supply one.
When rendering an included package, its placeholders are resolved from the
including package's already-resolved values by name, falling back to the
included package's own defaults. An included package with a required input
that the including package does not supply is an error naming both packages.
Implementations MUST detect inclusion cycles and report them rather than
recursing.
CPF does **not** define template inheritance. A package does not extend
another, override its sections, or inherit its inputs. Composition is by
reference only, for four reasons drawn from this specification and from
`INTENT.md`: hidden context defeats reuse (INTENT principle 3); § 19 asks that
package contents be visible before execution, which an inheritance chain
prevents; § 17 asks that behavior changes produce a new version, which an
inherited change would bypass; and resolving an override chain is the kind of
graph problem the non-goals exclude.
This allows richer systems to integrate prompt resolution without forcing simple tools to implement an agent runtime.
A prompt dependency declared `requirement: generate` is the mechanism behind a
@ -667,7 +780,13 @@ A v0.1 validator SHOULD verify at least:
11. no input declares both `default` and `required: true`;
12. a derived default declares either a `derive` reference or an inline
`derive.prompt`, never both;
13. a `derive` reference names a package declared in `dependencies.prompts`.
13. a `derive` or `include` reference names a package declared in
`dependencies.prompts`;
14. no default declares both `include` and `derive`;
15. every dependency referenced by a composition or derived default declares a
`version`, and that version is a semver literal, `any`, `newest`, or a
`>=` lower bound (§ 10.1);
16. inclusion does not form a cycle.
A validator SHOULD additionally warn when a package intended for publication
declares an inline derivation prompt (§ 6.1).
@ -837,6 +956,10 @@ rather than resolved by guessing.
`add` takes a package from a path rather than from a registry, so it files the
package under the reserved registry name `local`.
The reference tool satisfies `include` defaults, because inclusion is
deterministic and needs no model, and reports `derive` defaults it cannot
satisfy.
The reference tool never calls a model, so its `resolve` handles supplied
values and static defaults only and reports any input whose derived default it
cannot satisfy. `render` performs the same resolution and then substitutes;