CANP-WP-0002 T01: input defaults, static and derived

Closes the gap that made optional inputs unusable: rendering rule 5.1(4)
made any unresolved placeholder an error while inputs had no `default`, so
an input marked `required: false` and referenced from the template failed
every render in which the caller omitted it — including the spec's own
section 4 example.

Section 10 already carried the derivation mechanism (`requirement: generate`,
resolution deliberately undefined), so a derived default needed a binding
rather than a new concept: the input's default names a declared prompt
dependency.

Spec:
- 5.1 rewritten as "Resolution and rendering". Resolution may be
  non-deterministic and must report what it derived; rendering is
  deterministic and must not derive. A tool that handles only supplied
  values and static defaults is stated to be conforming.
- 6.1 (new) covers both declaration forms. Reference form is preferred, with
  the reason stated — an inline prompt is anonymous, so unversioned,
  unprovenanced and un-evaluable — and validators should warn when a
  published package derives inline.
- A derived default may declare a static fallback `value`. Without one the
  input stays unresolved, which is an error; derivation never silently
  yields empty content.
- 4, 10, 18 (rules 11-13), 19 (two new MUST NOTs), 21 updated accordingly.

Reference CLI:
- New `resolve` verb reporting the origin of every value.
- `Resolution` dataclass and `resolve_inputs`; `resolve_values` kept as a
  wrapper so existing callers are unaffected.
- `render` refuses with a specific error naming underivable inputs rather
  than substituting empty text.
- Tests 3 -> 11. Example package lifecycle re-verified end to end.

INTENT.md is unchanged: splitting resolve from render preserves success
criterion 4 (deterministic rendering) as written.

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 00:59:20 +02:00
parent 0caf065544
commit 169db25d25
6 changed files with 600 additions and 36 deletions

View file

@ -163,6 +163,9 @@ inputs:
type: content
required: false
description: Optional surrounding repository context.
default:
derive: context/repository-summary
value: "(no repository context provided)"
parameters:
depth:
@ -186,7 +189,10 @@ compatibility:
providers: []
dependencies:
prompts: []
prompts:
- id: context/repository-summary
version: 1.0.0
requirement: generate
context: []
capabilities: []
@ -235,14 +241,44 @@ Review the following change at {{ depth }} depth.
{{ change }}
```
### 5.1 Rendering rules
### 5.1 Resolution and rendering
Producing a final prompt is two steps:
1. **Resolve** — determine a value for every declared input and parameter.
2. **Render** — substitute those values into the template as text.
The steps are separate because only the first may be non-deterministic.
**Rendering is deterministic:** the same resolved values and the same template
always produce the same output. A consumer that satisfies a derived default
(§ 6.1) does so during resolution, never during rendering.
**Resolution rules:**
1. Call-supplied values override defaults.
2. A declared parameter default is used when no call value is supplied.
3. A required input without a value is an error.
4. A placeholder with no resolved value is an error.
5. Values are substituted as text in v0.1.
6. Template evaluation MUST NOT execute arbitrary code.
3. A declared static input default is used when no call value is supplied.
4. A required input without a value is an error.
5. A derived input default is satisfied only by a consumer that is able and
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.
**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
it as a distinct resolve step whose results are visible to the caller
before rendering.
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.
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.
@ -266,6 +302,7 @@ Fields:
| `type` | no | Suggested semantic type; defaults to `content` |
| `required` | no | Whether a caller must supply it; defaults to `false` |
| `description` | no | Human-readable explanation |
| `default` | no | Value used when the caller supplies none; see § 6.1 |
Recommended v0.1 input types are:
@ -277,6 +314,94 @@ Recommended v0.1 input types are:
These are descriptive hints in v0.1. A runtime MAY use them for validation or adapters.
### 6.1 Input defaults
An input MAY declare a `default`, used when the caller supplies no value.
`default` MUST NOT be combined with `required: true`: a required input is always
supplied by the caller, so a default could never apply.
Without this field an optional input is close to unusable. Rendering rule 8
makes an unresolved placeholder an error, so an input marked `required: false`
and referenced from the template would fail every render in which the caller
omitted it.
A default takes one of two forms.
#### Static default
A literal value, used as-is:
```yaml
inputs:
- name: repository_context
type: content
required: false
default: "(no repository context provided)"
```
Every conforming implementation supports static defaults.
#### Derived default
A declaration that the value **may** be produced from available context by a
consumer able to do so. It is a request to the consumer, not an instruction the
package executes.
The preferred form references a package already declared in
`dependencies.prompts` with `requirement: generate` (§ 10):
```yaml
dependencies:
prompts:
- id: context/repository-summary
version: 1.0.0
requirement: generate
inputs:
- name: repository_context
type: content
required: false
default:
derive: context/repository-summary
value: "(no repository context provided)"
```
A derivation prompt MAY instead be written inline:
```yaml
inputs:
- name: repository_context
type: content
required: false
default:
derive:
prompt: |
Summarize the repository this prompt is being run against,
in under 200 words.
value: "(no repository context provided)"
```
`derive` is therefore either a string naming a declared prompt dependency, or a
mapping carrying an inline `prompt`. A single default MUST NOT use both.
Prefer the reference form wherever the derivation is worth keeping. An inline
prompt is anonymous: it has no version, provenance, examples or evals, and
cannot be reused, evaluated or improved independently of its host package —
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
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.
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
runtime. Resolution belongs to the consumer; the package only declares what it
would like resolved.
## 7. Parameters
`parameters` is an optional mapping keyed by parameter name.
@ -377,6 +502,12 @@ Recommended requirement values:
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
referenced derived default (§ 6.1): the input's `default.derive` names the
dependency, and a consumer able to generate satisfies both at once. Declaring
the dependency records *what* may be generated and at which version; the input
default records *where the result lands*. Neither states how generation works.
## 11. Examples
`examples` is a list of relative paths.
@ -492,7 +623,14 @@ A v0.1 validator SHOULD verify at least:
7. referenced example/eval paths do not escape the package;
8. required inputs and parameter names are unique;
9. every template placeholder resolves to a declared input or parameter;
10. no required value is silently omitted during rendering.
10. no required value is silently omitted during rendering;
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`.
A validator SHOULD additionally warn when a package intended for publication
declares an inline derivation prompt (§ 6.1).
## 19. Security requirements
@ -502,6 +640,11 @@ Implementations MUST NOT:
- execute code merely because it appears in a package;
- treat template expressions as arbitrary code;
- treat a derived default's prompt text as instructions addressed to the
consuming tool itself; it is content to be resolved on the package's behalf,
and it carries no more authority than any other package text;
- derive an input default without the caller being able to see that it
happened (§ 5.1 rule 6);
- interpolate environment variables or credentials implicitly;
- follow paths outside the package without explicit user action;
- embed or require secrets in published package metadata.
@ -566,11 +709,17 @@ Commands:
add PATH validate and copy a package into the local catalog
search QUERY search locally installed package metadata
show ID display one installed package manifest
resolve ID report the resolved value of every input and parameter
render ID render an installed prompt with supplied values
publish PATH validate and copy a package into a filesystem registry
install ID copy a package version from the registry into the catalog
```
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;
per § 5.1 rule 10 it never derives.
These semantics are illustrative, not mandatory for other implementations.
## 22. Worked example

View file

@ -24,6 +24,10 @@ python canned_prompts.py add ../examples/pqrst-estimate
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."
@ -50,6 +54,19 @@ 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.
## 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.

View file

@ -2,12 +2,13 @@
This is intentionally a **small reference implementation**, not the intended final architecture.
It demonstrates six verbs:
It demonstrates seven verbs:
```text
add PATH
search QUERY
show ID
resolve ID --set key=value
render ID --set key=value
install ID [--version VERSION]
publish PATH
@ -15,6 +16,11 @@ 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:
@ -44,6 +50,10 @@ For example:
- Published versions are immutable by default.
- `install` copies from registry to catalog.
- `add` copies a package directly to catalog.
- `search`, `show`, and `render` operate on catalog packages.
- `search`, `show`, `resolve`, and `render` operate on catalog packages.
- 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.

View file

@ -13,6 +13,7 @@ import os
import re
import shutil
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Iterable
@ -92,6 +93,78 @@ def declared_names(manifest: dict[str, Any]) -> set[str]:
return names
def input_default_kind(item: dict[str, Any]) -> str:
"""Classify an input's default as 'none', 'static', or 'derived' (§ 6.1)."""
if "default" not in item:
return "none"
default = item["default"]
if isinstance(default, dict) and "derive" in default:
return "derived"
return "static"
def prompt_dependency_ids(manifest: dict[str, Any]) -> set[str]:
dependencies = manifest.get("dependencies") or {}
if not isinstance(dependencies, dict):
raise CannedPromptError("dependencies must be a mapping")
prompts = dependencies.get("prompts") or []
if not isinstance(prompts, list):
raise CannedPromptError("dependencies.prompts must be a list")
ids: set[str] = set()
for entry in prompts:
if isinstance(entry, str):
ids.add(entry)
elif isinstance(entry, dict) and isinstance(entry.get("id"), str):
ids.add(entry["id"])
else:
raise CannedPromptError(
"each prompt dependency must be an id string or a mapping with a string id"
)
return ids
def validate_input_default(item: dict[str, Any], dependency_ids: set[str]) -> None:
"""Validation rules 11-13 of § 18."""
kind = input_default_kind(item)
if kind == "none":
return
name = item["name"]
if item.get("required", False):
raise CannedPromptError(
f"input {name!r} declares a default and required: true; a required "
"input is always supplied, so the default could never apply"
)
if kind == "static":
return
derive = item["default"]["derive"]
if isinstance(derive, str):
if not derive.strip():
raise CannedPromptError(f"input {name!r}: derive reference is empty")
if derive not in dependency_ids:
raise CannedPromptError(
f"input {name!r} derives from {derive!r}, which is not declared "
"in dependencies.prompts"
)
return
if isinstance(derive, dict):
prompt = derive.get("prompt")
if not isinstance(prompt, str) or not prompt.strip():
raise CannedPromptError(
f"input {name!r}: inline derive requires a non-empty prompt"
)
extra = sorted(set(derive) - {"prompt"})
if extra:
raise CannedPromptError(
f"input {name!r}: an inline derive cannot also reference a "
"dependency; unexpected keys: " + ", ".join(extra)
)
return
raise CannedPromptError(
f"input {name!r}: derive must be a dependency id or a mapping with a prompt"
)
def validate_package(package_dir: Path) -> dict[str, Any]:
package_dir = package_dir.resolve()
if not package_dir.is_dir():
@ -127,6 +200,11 @@ def validate_package(package_dir: Path) -> dict[str, Any]:
safe_relative_file(package_dir, relative, field)
names = declared_names(manifest)
dependency_ids = prompt_dependency_ids(manifest)
for item in manifest.get("inputs") or []:
validate_input_default(item, dependency_ids)
template = template_path.read_text(encoding="utf-8")
placeholders = set(PLACEHOLDER_RE.findall(template))
undeclared = sorted(placeholders - names)
@ -305,8 +383,23 @@ def supplied_values(pairs: list[str]) -> dict[str, str]:
return values
def resolve_values(manifest: dict[str, Any], raw_values: dict[str, str]) -> dict[str, Any]:
resolved: dict[str, Any] = {}
@dataclass
class Resolution:
"""The outcome of § 5.1 resolution, separate from rendering."""
values: dict[str, Any] = field(default_factory=dict)
origins: dict[str, str] = field(default_factory=dict)
underivable: list[str] = field(default_factory=list)
def resolve_inputs(manifest: dict[str, Any], raw_values: dict[str, str]) -> Resolution:
"""Resolve inputs and parameters per § 5.1.
This tool never calls a model, so a derived default is satisfied only by its
static fallback `value`. An input with a derived default and no fallback is
reported as underivable rather than silently rendered as empty.
"""
result = Resolution()
known: set[str] = set()
inputs = manifest.get("inputs") or []
@ -314,23 +407,48 @@ def resolve_values(manifest: dict[str, Any], raw_values: dict[str, str]) -> dict
name = item["name"]
known.add(name)
if name in raw_values:
resolved[name] = raw_values[name]
elif item.get("required", False):
result.values[name] = raw_values[name]
result.origins[name] = "supplied"
continue
if item.get("required", False):
raise CannedPromptError(f"missing required input: {name}")
kind = input_default_kind(item)
if kind == "static":
result.values[name] = item["default"]
result.origins[name] = "default"
elif kind == "derived":
default = item["default"]
if "value" in default:
result.values[name] = default["value"]
result.origins[name] = "fallback (not derived)"
else:
result.origins[name] = "unresolved (derived, no fallback)"
result.underivable.append(name)
else:
result.origins[name] = "unresolved (optional, no default)"
parameters = manifest.get("parameters") or {}
for name, spec in parameters.items():
known.add(name)
if name in raw_values:
resolved[name] = coerce_value(raw_values[name], spec)
result.values[name] = coerce_value(raw_values[name], spec)
result.origins[name] = "supplied"
elif "default" in spec:
resolved[name] = spec["default"]
result.values[name] = spec["default"]
result.origins[name] = "default"
else:
result.origins[name] = "unresolved (no default)"
unknown = sorted(set(raw_values) - known)
if unknown:
raise CannedPromptError("unknown values: " + ", ".join(unknown))
return resolved
return result
def resolve_values(manifest: dict[str, Any], raw_values: dict[str, str]) -> dict[str, Any]:
return resolve_inputs(manifest, raw_values).values
def render_template(template: str, values: dict[str, Any]) -> str:
@ -354,11 +472,65 @@ def cmd_render(args: argparse.Namespace) -> None:
manifest = validate_package(package_dir)
template_path = safe_relative_file(package_dir, manifest["template"], "template")
raw = supplied_values(args.set_values)
values = resolve_values(manifest, raw)
rendered = render_template(template_path.read_text(encoding="utf-8"), values)
resolution = resolve_inputs(manifest, raw)
template = template_path.read_text(encoding="utf-8")
used = set(PLACEHOLDER_RE.findall(template))
blocked = sorted(used.intersection(resolution.underivable))
if blocked:
raise CannedPromptError(
"cannot render: this tool does not derive values, and these inputs "
"declare a derived default with no static fallback: "
+ ", ".join(blocked)
+ " — supply them with --set, or add a fallback `value` to the default"
)
rendered = render_template(template, resolution.values)
print(rendered, end="" if rendered.endswith("\n") else "\n")
def cmd_resolve(args: argparse.Namespace) -> None:
catalog = Path(args.catalog).expanduser()
package_dir = resolve_installed(catalog, args.id, args.version)
manifest = validate_package(package_dir)
raw = supplied_values(args.set_values)
resolution = resolve_inputs(manifest, raw)
if args.json:
print(
json.dumps(
{
"values": resolution.values,
"origins": resolution.origins,
"underivable": resolution.underivable,
},
indent=2,
ensure_ascii=False,
)
)
return
width = max((len(name) for name in resolution.origins), default=0)
for name, origin in resolution.origins.items():
if name in resolution.values:
preview = str(resolution.values[name]).replace("\n", " ")
if len(preview) > 48:
preview = preview[:45] + "..."
print(f"{name:<{width}} {origin:<32} {preview}")
else:
print(f"{name:<{width}} {origin}")
if resolution.underivable:
print()
plural = len(resolution.underivable) > 1
print(
"note: "
+ ", ".join(resolution.underivable)
+ (" declare" if plural else " declares")
+ " a derived default this tool cannot satisfy; rendering will fail "
+ ("if the template uses them" if plural else "if the template uses it")
)
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(prog="canned-prompts", description=__doc__)
sub = parser.add_subparsers(dest="command", required=True)
@ -388,6 +560,14 @@ def build_parser() -> argparse.ArgumentParser:
render.add_argument("--set", dest="set_values", action="append", default=[], metavar="NAME=VALUE")
render.set_defaults(func=cmd_render)
resolve = sub.add_parser("resolve", help="report how each input and parameter resolves")
resolve.add_argument("id")
resolve.add_argument("--version")
resolve.add_argument("--catalog", default=str(default_catalog()))
resolve.add_argument("--set", dest="set_values", action="append", default=[], metavar="NAME=VALUE")
resolve.add_argument("--json", action="store_true")
resolve.set_defaults(func=cmd_resolve)
install = sub.add_parser("install", help="install a package from a filesystem registry")
install.add_argument("id")
install.add_argument("--version")

View file

@ -51,3 +51,172 @@ def test_undeclared_placeholder_fails(package: Path) -> None:
(package / "prompt.md").write_text("{{ missing }}\n", encoding="utf-8")
with pytest.raises(cp.CannedPromptError, match="undeclared placeholders"):
cp.validate_package(package)
def write_pkg(pkg: Path, manifest: str, template: str = "{{ greeting }}\n") -> Path:
pkg.mkdir(exist_ok=True)
(pkg / "prompt.yaml").write_text(manifest, encoding="utf-8")
(pkg / "prompt.md").write_text(template, encoding="utf-8")
return pkg
BASE = """\
format: canned-prompt/v0.1
id: demo/defaults
name: Defaults
version: 1.0.0
summary: Exercise input defaults.
template: prompt.md
"""
def test_static_default_fills_optional_input(tmp_path: Path) -> None:
pkg = write_pkg(
tmp_path / "p",
BASE
+ """\
inputs:
- name: greeting
required: false
default: "hello there"
""",
)
manifest = cp.validate_package(pkg)
resolution = cp.resolve_inputs(manifest, {})
assert resolution.values["greeting"] == "hello there"
assert resolution.origins["greeting"] == "default"
assert cp.render_template("{{ greeting }}", resolution.values) == "hello there"
def test_supplied_value_overrides_static_default(tmp_path: Path) -> None:
pkg = write_pkg(
tmp_path / "p",
BASE
+ """\
inputs:
- name: greeting
required: false
default: "hello there"
""",
)
manifest = cp.validate_package(pkg)
resolution = cp.resolve_inputs(manifest, {"greeting": "hi"})
assert resolution.values["greeting"] == "hi"
assert resolution.origins["greeting"] == "supplied"
def test_derived_default_uses_static_fallback(tmp_path: Path) -> None:
pkg = write_pkg(
tmp_path / "p",
BASE
+ """\
dependencies:
prompts:
- id: context/greeting
version: 1.0.0
requirement: generate
inputs:
- name: greeting
required: false
default:
derive: context/greeting
value: "(none)"
""",
)
manifest = cp.validate_package(pkg)
resolution = cp.resolve_inputs(manifest, {})
assert resolution.values["greeting"] == "(none)"
assert resolution.origins["greeting"] == "fallback (not derived)"
assert resolution.underivable == []
def test_derived_default_without_fallback_is_underivable(tmp_path: Path) -> None:
pkg = write_pkg(
tmp_path / "p",
BASE
+ """\
dependencies:
prompts:
- id: context/greeting
version: 1.0.0
requirement: generate
inputs:
- name: greeting
required: false
default:
derive: context/greeting
""",
)
manifest = cp.validate_package(pkg)
resolution = cp.resolve_inputs(manifest, {})
assert resolution.underivable == ["greeting"]
assert "greeting" not in resolution.values
with pytest.raises(cp.CannedPromptError, match="unresolved placeholder"):
cp.render_template("{{ greeting }}", resolution.values)
def test_inline_derive_is_valid(tmp_path: Path) -> None:
pkg = write_pkg(
tmp_path / "p",
BASE
+ """\
inputs:
- name: greeting
required: false
default:
derive:
prompt: Produce a greeting suited to the audience.
value: "(none)"
""",
)
manifest = cp.validate_package(pkg)
assert cp.resolve_inputs(manifest, {}).values["greeting"] == "(none)"
def test_default_with_required_true_fails(tmp_path: Path) -> None:
pkg = write_pkg(
tmp_path / "p",
BASE
+ """\
inputs:
- name: greeting
required: true
default: "hello"
""",
)
with pytest.raises(cp.CannedPromptError, match="required: true"):
cp.validate_package(pkg)
def test_derive_reference_must_be_declared(tmp_path: Path) -> None:
pkg = write_pkg(
tmp_path / "p",
BASE
+ """\
inputs:
- name: greeting
required: false
default:
derive: context/greeting
""",
)
with pytest.raises(cp.CannedPromptError, match="not declared in dependencies"):
cp.validate_package(pkg)
def test_inline_derive_cannot_also_reference(tmp_path: Path) -> None:
pkg = write_pkg(
tmp_path / "p",
BASE
+ """\
inputs:
- name: greeting
required: false
default:
derive:
prompt: Produce a greeting.
id: context/greeting
""",
)
with pytest.raises(cp.CannedPromptError, match="cannot also reference"):
cp.validate_package(pkg)

View file

@ -40,7 +40,7 @@ or execution engine that satisfies it.
```task
id: CANP-WP-0002-T01
status: todo
status: done
priority: high
state_hub_task_id: "e49bf036-0aee-5bb2-abbd-3d3f683df298"
```
@ -62,22 +62,56 @@ allow that default to be either
Unresolved-with-no-default remains an error.
**Design note.** § 10 already carried the derivation mechanism:
`dependencies` accepts `requirement: generate`, defined as "a resolver MAY
satisfy a missing dependency by invoking an appropriate generation process",
with resolution left undefined. A derived default therefore did not need a new
concept, only a binding — the input's default names the dependency, and the
dependency records what may be generated and at which version.
**Follow-up decisions (operator, 2026-09-06):**
- *Both declaration forms are allowed.* A derived default may reference a
declared prompt dependency, or carry an inline `prompt`. The reference form
is preferred and the spec says why (an inline prompt is anonymous —
unversioned, unprovenanced, un-evaluable), and a validator SHOULD warn when a
**published** package derives inline. Inline is for local and draft packages.
- *Error unless static fallback.* A derived default MAY declare a static
`value`. A consumer that does not derive uses it; with no fallback the input
stays unresolved, which is an error. Derivation never silently yields empty
content.
- *Split render from resolve.* Resolution decides values and may be
non-deterministic; rendering substitutes and always is. Rendering MUST NOT
derive. This preserves INTENT success criterion 4 verbatim — no change to
`INTENT.md` was needed.
Work:
1. Extend § 6 with `default`, and state the two default kinds.
2. Specify the derived-default declaration form so it stays inert data: the
package declares *what* to derive and the prompt to derive it with; it never
names or requires a specific resolver, model, or runtime. A consumer that
cannot derive must report the input as unresolved rather than guess.
3. Reconcile § 5.1: rules 12 gain input defaults; rule 4 keeps unresolved as
an error; § 5.1's "no conditionals, loops, filters, or functions" and
§ 19's "MUST NOT execute code merely because it appears in a package" must
survive the change — a derived default is a *request to a consumer*, not
template-embedded execution. Say so explicitly.
4. Extend § 18 validation accordingly.
5. Implement static defaults in `reference/canned_prompts.py` `resolve_values`
and add tests. Derived defaults: validate and surface them; the reference
CLI never calls a model, so it reports them as unresolved-by-design.
Delivered:
1. § 5.1 rewritten as "Resolution and rendering" — six resolution rules and
four rendering rules, with rendering explicitly deterministic and forbidden
from deriving (rule 10). Resolution must report which values were derived
(rule 6). A tool that resolves only supplied values and static defaults is
stated to be conforming.
2. § 6 gains `default` in the field table plus a new § 6.1 covering both
declaration forms, the static fallback, and why the reference form is
preferred.
3. § 4 manifest surface updated: `repository_context` — the field that
demonstrated the original defect — now carries a derived default with a
fallback, backed by a `requirement: generate` prompt dependency.
4. § 10 states the binding between `requirement: generate` and a referenced
derived default.
5. § 18 gains validation rules 1113 and the publish-time inline warning.
6. § 19 gains two MUST NOTs: a derived default's prompt text is not an
instruction addressed to the consuming tool, and derivation must be visible
to the caller.
7. § 21 documents the new `resolve` verb.
8. `reference/canned_prompts.py`: `Resolution` dataclass, `resolve_inputs`
(with `resolve_values` kept as a wrapper), `input_default_kind`,
`prompt_dependency_ids`, `validate_input_default`, a `resolve` command, and
a specific render-time error naming the underivable inputs. Tests: 3 → 11,
all passing. Both READMEs document the split.
## Registry namespaces and ownership
@ -113,6 +147,10 @@ capture") and the `dependencies.prompts` field both promise composition, but
v0.1 defines no mechanism — `dependencies` is a declared field with no
semantics.
T01 has since settled one corner of this: a derived default binds an input to
a prompt dependency declared `requirement: generate`, so package-to-package
reference already exists for that one case. Build on it rather than around it.
Decide what composition means at the *artifact* level: how one package
references another, whether references are includes, extends, or plain
declared prerequisites, and how versions are pinned. Leaning: declaration only
@ -154,9 +192,10 @@ manifest surface with no semantics whatsoever in v0.1, and § 9
Decide: what a context dependency declares, how it differs from an input, how
it relates to `compatibility.capabilities`, and whether capability names are
free strings in v0.2 (model capability vocabularies stay deferred). Coordinate
with T01 — a derived default is a consumer-resolved context requirement, and
the two mechanisms must not describe the same thing twice.
free strings in v0.2 (model capability vocabularies stay deferred). T01 is now settled and partly answers this: a derived default is a
consumer-resolved context requirement expressed through `dependencies.prompts`
rather than through `dependencies.context`. Decide whether `context` is still a
distinct concept or collapses into the prompt-dependency mechanism.
## Rewrite specification section 23