CANP-WP-0005: document and detect inclusion diamonds

Section 10.4 said nothing about inclusion being textual and repeated, so an
author met the behaviour only by reading rendered output carefully. That
silence was the defect.

Writing it up got the mechanism wrong at first. I documented a dependency
"reached by two paths" through two include chains, and wrote a detector that
counted composer calls to match. Tested it against the real case that motivated
the task — helix/repo-advance — and it did not fire, though the conventions
block still rendered twice.

The actual mechanism is subtler. helix/commit-sync declares its own
`conventions` input; when composed, resolution passes the including package's
already-resolved values down by name, so it *inherits* the outer text rather
than resolving its own include, and renders it again. Nothing was included
twice; the text appeared twice regardless.

Both mechanisms are now documented with worked diagrams. duplicate_inclusions
detects both — included values that are equal, and an included value contained
within another — and resolve, render and eval warn.

Nothing is deduplicated, as leaned. Deduplicating means choosing which
occurrence survives, since position in a prompt carries meaning, and deciding
what happens when two paths select different versions of the same dependency,
which section 10.3 permits. That is resolver behaviour and section 10.4 keeps
composition declarative.

Verified by reintroducing the diamond in a scratch copy of repo-advance, which
warns, and confirming the shipped factored collection stays silent.
Tests 90 -> 95.

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 19:23:16 +02:00
parent e16bf024d4
commit 75e0597019
5 changed files with 180 additions and 7 deletions

View file

@ -741,6 +741,60 @@ that the including package does not supply is an error naming both packages.
Implementations MUST detect inclusion cycles and report them rather than
recursing.
#### Inclusion is textual and repeated
Inclusion is not deduplicated, and there are two ways the same text ends up
rendered twice.
**Two inputs include the same dependency.** The values are equal and both are
substituted:
```text
review/change
├── style → include style/house
└── preamble → include style/house ← the same text again
```
**An included package inherits an outer input of the same name.** This one is
easy to miss, because nothing was included twice:
```text
review/change
├── conventions → include team/conventions ← rendered here
└── routine → include review/commit-routine
└── {{ conventions }} ← inherited (§ 10.4), rendered again
```
`review/commit-routine` declares its own `conventions` input. When it is
composed, resolution passes the including package's already-resolved values
down by name, so it inherits the outer text rather than resolving its own — and
renders it a second time. No dependency was included twice; the text still
appears twice.
Nothing is wrong with either package. The duplication is a property of how they
were composed.
Deduplication is deliberately not specified. It would require deciding which
occurrence survives — position in a prompt carries meaning — and what happens
when the two paths select *different versions* of the same dependency (§ 10.3
permits that: one path may pin `1.0.0` while another asks for `newest`).
Resolving that is a resolver's job, and § 10.4 keeps composition declarative.
The fix belongs to the author, and it is usually a better factoring. Extract the
part that is genuinely shared into its own fragment and include it once at each
level that needs it, rather than composing a whole package that carries it:
```text
review/change
├── style → include style/house
└── routine → include review/commit-routine (no style of its own)
```
A tool that renders a package SHOULD report when an included value will render
more than once — whether because two inputs resolved to the same text, or
because one included value contains another — since the author usually did not
intend it.
CPF does **not** define template inheritance. A package does not extend
another, override its sections, or inherit its inputs. Composition is by
reference only, for four reasons drawn from this specification and from