Section 17 already defined what a registry does when the same id@version is republished; nothing defined what a *consumer* does. That is where namespace conflict actually bites — in the catalog, after installing from two registries. Identity is now registry-scoped: an id names a package within a registry, the way a path names a file within a repository. A local-first format with no signing, no federation and no central authority cannot enforce global uniqueness, and an unenforceable guarantee is worse than none — it invites consumers to conflate two packages that merely share a name. A qualified `<registry>:<id>` reference distinguishes them, and `:` is now barred from ids so the separator stays available. Installing the same id from two registries is therefore not a conflict. The catalog is namespaced by registry and keeps both. Ownership is registry policy, not package data. An optional `registry.yaml` names a registry and records namespace claims. Those claims are explicitly descriptive — a filesystem registry cannot authenticate a publisher, and `publish` says so rather than implying it checked. Keeping the claim out of packages leaves artifacts free of unverifiable assertions of authority, and means package semantics do not change when a hosted registry appears later. Spec: 3.2 (registry-scoped identity, qualified references), 17 (immutability scoped to a registry), 20.1 and 20.2 (new), 18 (registry-manifest validation), 21 (qualified references, reserved `local` name). Reference CLI: parse_reference, check_registry_name, read_registry_manifest, registry_name, namespace_policy; registry_package_path and catalog_package_path split; resolve_installed reports ambiguity and returns the source registry; iter_catalog; `add --as`; closed-namespace warning on publish. Tests 11 -> 21. The catalog layout changed. An existing catalog is detected and reported with instructions rather than failing as "package not found". Signing, trust scoring and federation remain non-goals and were not touched. 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
279 lines
12 KiB
Markdown
279 lines
12 KiB
Markdown
---
|
||
id: CANP-WP-0002
|
||
type: workplan
|
||
title: "Resolve CPF v0.1 open questions promoted for v0.2"
|
||
domain: agents
|
||
repo: canned-prompts
|
||
status: proposed
|
||
owner: codex
|
||
topic_slug: practice
|
||
created: "2026-09-06"
|
||
updated: "2026-09-06"
|
||
reviewed_at: "2026-09-06"
|
||
reviewed_by: "claude"
|
||
context_paths:
|
||
- "INTENT.md"
|
||
- "CannedPromptFormat-v0.1.md"
|
||
- "reference/canned_prompts.py"
|
||
- "examples/pqrst-estimate/"
|
||
state_hub_workstream_id: "c87c8e27-8b11-5306-b687-86779ef3f1be"
|
||
---
|
||
|
||
# Resolve CPF v0.1 open questions promoted for v0.2
|
||
|
||
`CannedPromptFormat-v0.1.md` § 23 currently lists twelve items as "experience
|
||
should determine". A review of the seed on 2026-09-06 (spec + reference CLI +
|
||
`examples/pqrst-estimate`; 3/3 tests pass, full local lifecycle smokes clean)
|
||
plus an operator interview promoted five of them from "wait and see" to
|
||
"decide, with a stated leaning". This workplan carries those decisions.
|
||
|
||
Unpromoted § 23 items stay deferred and unchanged: content macros, cryptographic
|
||
integrity/signing, model capability vocabularies, run manifests and evidence
|
||
formats, deterministic compilation manifests, trust/reputation signals,
|
||
federated discovery, richer template syntax.
|
||
|
||
**Guardrail for every task below:** `INTENT.md` § Deliberate boundary. The
|
||
format may *declare* a requirement; it must not specify the resolver, runtime,
|
||
or execution engine that satisfies it.
|
||
|
||
## Optional inputs need defaults
|
||
|
||
```task
|
||
id: CANP-WP-0002-T01
|
||
status: done
|
||
priority: high
|
||
state_hub_task_id: "e49bf036-0aee-5bb2-abbd-3d3f683df298"
|
||
```
|
||
|
||
**Gap found in review.** Rendering rule § 5.1(4) makes any unresolved
|
||
placeholder an error, and § 6 gives `inputs` no `default` field. An input
|
||
declared `required: false` and referenced from the template therefore makes
|
||
`render` fail unless a value is supplied — optional inputs are effectively
|
||
unusable. The spec's own § 4 manifest example hits this with
|
||
`repository_context`. `reference/canned_prompts.py` is conformant here; the
|
||
defect is in the spec.
|
||
|
||
**Decision (operator, 2026-09-06):** add a `default` field to `inputs`, and
|
||
allow that default to be either
|
||
|
||
- a **static** value, or
|
||
- a **derived** default — a prompt that produces the value from available
|
||
context.
|
||
|
||
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.
|
||
|
||
## Registry namespaces and ownership
|
||
|
||
```task
|
||
id: CANP-WP-0002-T02
|
||
status: done
|
||
priority: high
|
||
state_hub_task_id: "a52cd41e-2bcc-50ee-96a1-d645a06df922"
|
||
```
|
||
|
||
An id like `practice/pqrst-estimate` has a namespace prefix with no owner. Two
|
||
authors publishing `practice/…` into one registry collide today; `add` and
|
||
`publish` only refuse an exact `id@version` that already exists.
|
||
|
||
**Sharpened during the work.** § 17 already said what a *registry* does on
|
||
collision; nothing said what a *consumer* does. That is where the conflict
|
||
actually bites — in the catalog, after installing from two registries.
|
||
|
||
**Decisions (operator, 2026-09-06):**
|
||
|
||
- *Identity is registry-scoped.* An id names a package within a registry, the
|
||
way a path names a file within a repository. The same id from two registries
|
||
may be two different packages. A local-first format with no signing, no
|
||
federation and no central authority cannot enforce global uniqueness, and an
|
||
unenforceable guarantee is worse than none — it invites consumers to
|
||
conflate two packages that merely share a name. A qualified
|
||
`<registry>:<id>` reference distinguishes them.
|
||
- *Ownership is registry policy.* An optional `registry.yaml` names the
|
||
registry and records namespace claims (`owner`, `policy: open | closed`).
|
||
Packages never assert who owns their namespace, keeping unverifiable
|
||
authority claims out of artifacts (§ 19) and leaving package semantics
|
||
unchanged when a filesystem registry is later replaced by a hosted one.
|
||
- *Installing the same id from two registries is allowed, not a conflict.* The
|
||
catalog is namespaced by registry, so both are retained. This follows from
|
||
registry-scoped identity rather than working around it: two packages that
|
||
merely share a name were never in conflict to begin with.
|
||
|
||
Delivered:
|
||
|
||
1. § 3.2 gains "Identity is registry-scoped" — the reasoning, the qualified
|
||
reference form, and a ban on `:` in ids so the separator stays available.
|
||
A tool finding one id in several registries MUST report the ambiguity.
|
||
2. § 17 scopes immutability to a registry, consistent with identity.
|
||
3. § 20.1 (new) specifies the optional `registry.yaml`: `format`, `name`,
|
||
`description`, `namespaces`. Claims are explicitly descriptive — a
|
||
filesystem registry cannot authenticate a publisher. A bare directory
|
||
remains a valid registry, named by its basename.
|
||
4. § 20.2 (new) requires consumers to keep registries distinct and documents
|
||
the reference catalog layout.
|
||
5. § 18 gains registry-manifest validation; § 21 documents qualified
|
||
references and the reserved `local` registry name.
|
||
6. `reference/canned_prompts.py`: `parse_reference`, `check_registry_name`,
|
||
`read_registry_manifest`, `registry_name`, `namespace_policy`, split
|
||
`registry_package_path` / `catalog_package_path`, a `resolve_installed`
|
||
that reports ambiguity and returns the source registry, `iter_catalog`,
|
||
`--as` on `add`, a publish-time warning on closed namespaces, and a
|
||
specific error for the pre-registry-scoped catalog layout. Tests 11 → 21.
|
||
|
||
**Migration note:** the catalog layout changed. An existing catalog from
|
||
before this change is detected and reported with instructions rather than
|
||
failing as "package not found". Signing, trust scoring and federation remain
|
||
non-goals and were not touched.
|
||
|
||
## Prompt composition and inheritance
|
||
|
||
```task
|
||
id: CANP-WP-0002-T03
|
||
status: todo
|
||
priority: high
|
||
state_hub_task_id: "0505c790-e57e-568c-9c8c-376957c9b08e"
|
||
```
|
||
|
||
Largest gap between `INTENT.md` and the spec. Principle 9 ("composition without
|
||
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
|
||
— a package states what it needs, and resolution stays with the consumer, per
|
||
the INTENT boundary. Do not introduce range resolution (INTENT non-goal).
|
||
|
||
## Canonical eval schemas
|
||
|
||
```task
|
||
id: CANP-WP-0002-T04
|
||
status: todo
|
||
priority: medium
|
||
state_hub_task_id: "0daf1097-6599-563f-8965-739cbc874ddf"
|
||
```
|
||
|
||
Cheapest item to pin down. `evals/` is a reserved path and `evals:` is a
|
||
manifest list, but § 12 defines no schema, so an eval file is an unvalidated
|
||
blob that no tool can act on.
|
||
|
||
Define a minimal eval-spec schema: identity, what is being asserted, the
|
||
fixture it runs against, and how a result is reported. Keep it declarative and
|
||
engine-neutral — "universal prompt evaluation" is an explicit INTENT non-goal,
|
||
so this specifies the *file*, not an evaluation engine. Extend § 18 to validate
|
||
eval files that declare the schema, and add one eval to
|
||
`examples/pqrst-estimate` as a worked case.
|
||
|
||
## Typed context and dependency contracts
|
||
|
||
```task
|
||
id: CANP-WP-0002-T05
|
||
status: todo
|
||
priority: medium
|
||
state_hub_task_id: "5beec7ba-e6c9-58b1-a065-b06aba015034"
|
||
```
|
||
|
||
`dependencies.context` and `dependencies.capabilities` appear in the § 4
|
||
manifest surface with no semantics whatsoever in v0.1, and § 9
|
||
`compatibility.capabilities` overlaps them without a stated relationship.
|
||
|
||
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.
|
||
|
||
## Rewrite specification section 23
|
||
|
||
```task
|
||
id: CANP-WP-0002-T06
|
||
status: todo
|
||
priority: medium
|
||
state_hub_task_id: "06032689-fc19-5fcd-a76e-5dbc87d02cd4"
|
||
```
|
||
|
||
After T01–T05 land, replace § 23's flat twelve-item list with two sections:
|
||
questions **being decided for v0.2** (each with its scoped question and stated
|
||
leaning, referencing this workplan) and questions **still deferred** (the eight
|
||
unpromoted items listed at the top of this file). Bump the spec status line if
|
||
the format revision warrants it.
|
||
|
||
## Reference implementation conformance fixes
|
||
|
||
```task
|
||
id: CANP-WP-0002-T07
|
||
status: todo
|
||
priority: low
|
||
state_hub_task_id: "f642abe6-8222-5958-88ee-7a53454f2344"
|
||
```
|
||
|
||
Two defects found in the same review, independent of the format questions:
|
||
|
||
1. **Prerelease versions sort as newest.** `parse_semver` in
|
||
`reference/canned_prompts.py` returns `(major, minor, patch, raw_string)`,
|
||
so `1.0.0-rc1` and `1.0.0` tie on the numeric fields and then compare as
|
||
strings — `"1.0.0-rc1" > "1.0.0"`. `resolve_installed` with no `--version`
|
||
therefore selects a prerelease over its own release. Order prerelease below
|
||
release, or state in § 17 that v0.1 ignores prerelease ordering.
|
||
2. **`copy_immutable` copies everything.** `add`/`publish` use
|
||
`shutil.copytree` over the whole source directory, so a stray `.git`,
|
||
`.venv`, or scratch file lands in the catalog and registry. § 2 says tools
|
||
MUST ignore unknown non-reserved files unless a manifest field references
|
||
them. Decide whether packaging is reserved-paths-only or an explicit
|
||
ignore list, then align the implementation and § 2.
|