diff --git a/CannedPromptFormat-v0.1.md b/CannedPromptFormat-v0.1.md index ad01f1a..a32ad18 100644 --- a/CannedPromptFormat-v0.1.md +++ b/CannedPromptFormat-v0.1.md @@ -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 diff --git a/README.md b/README.md index 3f69cbf..35dec48 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/reference/README.md b/reference/README.md index 3615dd3..1fe1360 100644 --- a/reference/README.md +++ b/reference/README.md @@ -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. diff --git a/reference/canned_prompts.py b/reference/canned_prompts.py index d3c3ba8..3a55179 100755 --- a/reference/canned_prompts.py +++ b/reference/canned_prompts.py @@ -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") diff --git a/reference/tests/test_canned_prompts.py b/reference/tests/test_canned_prompts.py index 094fada..458ab49 100644 --- a/reference/tests/test_canned_prompts.py +++ b/reference/tests/test_canned_prompts.py @@ -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) diff --git a/workplans/CANP-WP-0002-format-open-questions.md b/workplans/CANP-WP-0002-format-open-questions.md index 06d7b5f..2e7c8ba 100644 --- a/workplans/CANP-WP-0002-format-open-questions.md +++ b/workplans/CANP-WP-0002-format-open-questions.md @@ -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 1–2 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 11–13 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