diff --git a/CannedPromptFormat-v0.1.md b/CannedPromptFormat-v0.1.md index a32ad18..ad01f1a 100644 --- a/CannedPromptFormat-v0.1.md +++ b/CannedPromptFormat-v0.1.md @@ -163,9 +163,6 @@ inputs: type: content required: false description: Optional surrounding repository context. - default: - derive: context/repository-summary - value: "(no repository context provided)" parameters: depth: @@ -189,10 +186,7 @@ compatibility: providers: [] dependencies: - prompts: - - id: context/repository-summary - version: 1.0.0 - requirement: generate + prompts: [] context: [] capabilities: [] @@ -241,44 +235,14 @@ Review the following change at {{ depth }} depth. {{ change }} ``` -### 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:** +### 5.1 Rendering rules 1. Call-supplied values override defaults. 2. A declared parameter default is used when no call value is supplied. -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. +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. 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. @@ -302,7 +266,6 @@ 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: @@ -314,94 +277,6 @@ 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. @@ -502,12 +377,6 @@ 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. @@ -623,14 +492,7 @@ 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; -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). +10. no required value is silently omitted during rendering. ## 19. Security requirements @@ -640,11 +502,6 @@ 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. @@ -709,17 +566,11 @@ 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 35dec48..3f69cbf 100644 --- a/README.md +++ b/README.md @@ -24,10 +24,6 @@ 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." @@ -54,19 +50,6 @@ 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/WORK-RECORDS.md b/WORK-RECORDS.md index fff90f3..0271ab0 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -9,7 +9,7 @@ | Kind | ID | Status | Lane | Source | | --- | --- | --- | --- | --- | | workplan | CANP-WP-0001 | finished | — | workplans/CANP-WP-0001-statehub-bootstrap.md | -| workplan | CANP-WP-0002 | proposed | — | workplans/CANP-WP-0002-format-open-questions.md | +| workplan | CANP-WP-0002 | ready | — | workplans/CANP-WP-0002-format-open-questions.md | | task | CANP-WP-0001-T01 | done | — | workplans/CANP-WP-0001-statehub-bootstrap.md | | task | CANP-WP-0001-T02 | done | — | workplans/CANP-WP-0001-statehub-bootstrap.md | | task | CANP-WP-0001-T03 | done | — | workplans/CANP-WP-0001-statehub-bootstrap.md | diff --git a/reference/README.md b/reference/README.md index 1fe1360..3615dd3 100644 --- a/reference/README.md +++ b/reference/README.md @@ -2,13 +2,12 @@ This is intentionally a **small reference implementation**, not the intended final architecture. -It demonstrates seven verbs: +It demonstrates six verbs: ```text add PATH search QUERY show ID -resolve ID --set key=value render ID --set key=value install ID [--version VERSION] publish PATH @@ -16,11 +15,6 @@ 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: @@ -50,10 +44,6 @@ For example: - Published versions are immutable by default. - `install` copies from registry to catalog. - `add` copies a package directly to catalog. -- `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. +- `search`, `show`, and `render` operate on catalog packages. 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 3a55179..d3c3ba8 100755 --- a/reference/canned_prompts.py +++ b/reference/canned_prompts.py @@ -13,7 +13,6 @@ import os import re import shutil import sys -from dataclasses import dataclass, field from pathlib import Path from typing import Any, Iterable @@ -93,78 +92,6 @@ 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(): @@ -200,11 +127,6 @@ 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) @@ -383,23 +305,8 @@ def supplied_values(pairs: list[str]) -> dict[str, str]: return values -@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() +def resolve_values(manifest: dict[str, Any], raw_values: dict[str, str]) -> dict[str, Any]: + resolved: dict[str, Any] = {} known: set[str] = set() inputs = manifest.get("inputs") or [] @@ -407,48 +314,23 @@ def resolve_inputs(manifest: dict[str, Any], raw_values: dict[str, str]) -> Reso name = item["name"] known.add(name) if name in raw_values: - result.values[name] = raw_values[name] - result.origins[name] = "supplied" - continue - if item.get("required", False): + resolved[name] = raw_values[name] + elif 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: - result.values[name] = coerce_value(raw_values[name], spec) - result.origins[name] = "supplied" + resolved[name] = coerce_value(raw_values[name], spec) elif "default" in spec: - result.values[name] = spec["default"] - result.origins[name] = "default" - else: - result.origins[name] = "unresolved (no default)" + resolved[name] = spec["default"] unknown = sorted(set(raw_values) - known) if unknown: raise CannedPromptError("unknown values: " + ", ".join(unknown)) - return result - - -def resolve_values(manifest: dict[str, Any], raw_values: dict[str, str]) -> dict[str, Any]: - return resolve_inputs(manifest, raw_values).values + return resolved def render_template(template: str, values: dict[str, Any]) -> str: @@ -472,65 +354,11 @@ 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) - 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) + values = resolve_values(manifest, raw) + rendered = render_template(template_path.read_text(encoding="utf-8"), 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) @@ -560,14 +388,6 @@ 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 458ab49..094fada 100644 --- a/reference/tests/test_canned_prompts.py +++ b/reference/tests/test_canned_prompts.py @@ -51,172 +51,3 @@ 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 2e7c8ba..06d7b5f 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: done +status: todo priority: high state_hub_task_id: "e49bf036-0aee-5bb2-abbd-3d3f683df298" ``` @@ -62,56 +62,22 @@ 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: -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. +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. ## Registry namespaces and ownership @@ -147,10 +113,6 @@ 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 @@ -192,10 +154,9 @@ 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). 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. +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. ## Rewrite specification section 23