CANP-WP-0002 T07: semver precedence and strict packaging

Two defects from the original review of the seed.

Prerelease ordering was worse than first recorded. parse_semver returned
(major, minor, patch, raw_string), so 1.0.0-rc1 and 1.0.0 tied on the numeric
fields and then compared as strings — "1.0.0-rc1" > "1.0.0". A release
candidate therefore shadowed its own release for `newest` and for `>=`, not
just for the no-version case.

parse_semver now returns a SemVer section 11 precedence key: numeric fields, a
release/prerelease rank, then dot-separated prerelease identifiers with
numeric ones compared numerically. Build metadata is ignored. Beyond ordering,
prereleases are excluded from `any`, `newest` and `>=` entirely; only an exact
pin selects one, so publishing a release candidate never changes what existing
consumers resolve to. A package holding only prereleases now says so rather
than reporting a bare not-found.

Packaging copied the whole source directory, so a stray .git, virtualenv or
scratch file landed in the catalog and registry. Section 2 already required
otherwise — tools MUST ignore unknown non-reserved files unless a manifest
field references them — so this is conformance rather than a new rule. What is
new is that omissions are reported instead of silent:

  not packaged (not a reserved path, not referenced by the manifest): .git/, .venv/, notes.txt

LICENSE joins the reserved paths. Strict packaging would otherwise drop a
package's license text while faithfully copying its `license` field, which
contradicts section 14's instruction to surface licensing on publish and
install.

Spec: 2 (LICENSE, packaging obligation, reporting), 17.1 new, 10.3 note.

Reference CLI: parse_semver rewritten with is_prerelease; select_version and
pick_version updated; copy_package and report_skipped replace copy_immutable.
Tests 65 -> 78.

examples/pqrst-estimate carries a LICENSE and a license field, exercising the
new reserved path.

Also fixes a leaked loop variable in package_members that would have reported
a bad `template` path as an `evals` error.

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 09:32:42 +02:00
parent e885eb7b05
commit 95a8bb31d2
8 changed files with 306 additions and 34 deletions

View file

@ -388,22 +388,57 @@ the format revision warrants it.
```task
id: CANP-WP-0002-T07
status: todo
status: done
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.
1. **Prerelease versions sorted as newest.** Confirmed worse than first
recorded: `1.0.0-rc1` shadowed `1.0.0` for `newest` *and* `>= 1.0.0`, so a
release candidate hid its own release from every selector.
2. **`copy_immutable` copied everything**, so a stray `.git`, `.venv` or
scratch file landed in the catalog and registry.
**Decisions (operator, 2026-09-06):**
- *Prereleases are excluded unless named.* They sort below their release and
are skipped by `any`, `newest` and `>=`; only an exact pin selects one.
Publishing a release candidate therefore never changes what existing
consumers resolve to — the point of marking it a candidate. Matches npm and
cargo.
- *`LICENSE` joins the reserved paths.* Strict packaging would otherwise drop
a package's license text while faithfully copying its `license` field, which
contradicts § 14's instruction to surface licensing on publish and install.
The strict-versus-ignore-list question needed no decision: § 2 already
required it — "Tools MUST ignore unknown non-reserved files unless a manifest
field explicitly references them" — so packaging by reserved path is
conformance, not a new choice. What was worth adding is that omissions are
**reported**, never silent.
Delivered:
1. `parse_semver` now returns a SemVer § 11 precedence key: numeric fields,
then a release/prerelease rank, then dot-separated prerelease identifiers
with numeric ones compared numerically. Build metadata is ignored, so
`1.0.0+build.1` and `1.0.0+build.2` tie. `1.0.0-rc.10` correctly outranks
`1.0.0-rc.2`.
2. `select_version` excludes prereleases from `any`, `newest` and `>=`;
`pick_version` reports "only prerelease versions are available" rather than
a bare not-found.
3. `copy_package` replaces `copy_immutable`, copying reserved paths plus
manifest-referenced files only, and returning what it skipped;
`report_skipped` names those on stderr. `skipped_entries` reports top-level
names without walking into an ignored directory, so a stray virtualenv
costs nothing to skip.
4. § 2 gains `LICENSE`, the packaging obligation, and the reporting SHOULD;
§ 17.1 (new) specifies precedence and prerelease selection; § 10.3 notes
the exclusion.
5. `examples/pqrst-estimate` now carries a `LICENSE` and a `license: MIT`
field, exercising the new reserved path. Tests 65 → 78.
**Fixed while implementing.** `package_members` reused a loop variable as the
error label, so a bad `template` path would have been reported as an `evals`
error.