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:
parent
e885eb7b05
commit
95a8bb31d2
8 changed files with 306 additions and 34 deletions
|
|
@ -680,3 +680,85 @@ def test_capability_may_be_both_required_and_observed(tmp_path: Path) -> None:
|
|||
manifest = cp.validate_package(pkg)
|
||||
assert manifest["dependencies"]["capabilities"] == ["web-search"]
|
||||
assert "long-context" in manifest["compatibility"]["capabilities"]
|
||||
|
||||
|
||||
# --- semver precedence and prereleases (§ 17.1) ---
|
||||
|
||||
def test_release_outranks_its_prerelease() -> None:
|
||||
assert cp.parse_semver("1.0.0") > cp.parse_semver("1.0.0-rc1")
|
||||
|
||||
|
||||
def test_prerelease_ordering_follows_semver() -> None:
|
||||
ordered = sorted(
|
||||
["1.0.0", "1.0.0-rc.2", "1.0.0-rc.10", "1.0.0-alpha", "0.9.0"],
|
||||
key=cp.parse_semver,
|
||||
reverse=True,
|
||||
)
|
||||
assert ordered == ["1.0.0", "1.0.0-rc.10", "1.0.0-rc.2", "1.0.0-alpha", "0.9.0"]
|
||||
|
||||
|
||||
def test_build_metadata_is_ignored_for_precedence() -> None:
|
||||
assert cp.parse_semver("1.0.0+build.1") == cp.parse_semver("1.0.0+build.2")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("selector", ["newest", "any", ">=1.0.0", None])
|
||||
def test_prerelease_is_not_selected_implicitly(selector) -> None:
|
||||
available = ["1.1.0-rc1", "1.0.0"]
|
||||
assert cp.select_version(available, selector) == "1.0.0"
|
||||
|
||||
|
||||
def test_exact_pin_selects_a_prerelease() -> None:
|
||||
assert cp.select_version(["1.1.0-rc1", "1.0.0"], "1.1.0-rc1") == "1.1.0-rc1"
|
||||
|
||||
|
||||
def test_only_prereleases_available_selects_nothing() -> None:
|
||||
assert cp.select_version(["1.0.0-rc1"], "newest") is None
|
||||
|
||||
|
||||
def test_prerelease_only_package_reports_why(tmp_path: Path) -> None:
|
||||
catalog = tmp_path / "catalog"
|
||||
place(catalog, "local", "practice/thing", "1.0.0-rc1", MINIMAL.replace(
|
||||
"version: 1.0.0", "version: 1.0.0-rc1"), "{{ greeting }}\n")
|
||||
with pytest.raises(cp.CannedPromptError, match="only prerelease versions"):
|
||||
cp.resolve_installed(catalog, "practice/thing", None)
|
||||
|
||||
|
||||
# --- strict packaging (§ 2) ---
|
||||
|
||||
def test_only_reserved_and_referenced_paths_are_packaged(tmp_path: Path) -> None:
|
||||
src = write_evaluated(tmp_path / "src", RUBRIC)
|
||||
(src / "LICENSE").write_text("MIT\n", encoding="utf-8")
|
||||
(src / "notes.txt").write_text("scratch\n", encoding="utf-8")
|
||||
(src / ".git").mkdir()
|
||||
(src / ".git" / "config").write_text("junk\n", encoding="utf-8")
|
||||
|
||||
manifest = cp.validate_package(src)
|
||||
dst = tmp_path / "out"
|
||||
skipped = cp.copy_package(src, dst, manifest, "package")
|
||||
|
||||
packaged = sorted(p.relative_to(dst).as_posix() for p in dst.rglob("*") if p.is_file())
|
||||
assert packaged == [
|
||||
"LICENSE",
|
||||
"evals/quality.yaml",
|
||||
"examples/basic.yaml",
|
||||
"prompt.md",
|
||||
"prompt.yaml",
|
||||
]
|
||||
assert skipped == [".git/", "notes.txt"]
|
||||
|
||||
|
||||
def test_packaged_copy_is_still_valid(tmp_path: Path) -> None:
|
||||
src = write_evaluated(tmp_path / "src", RUBRIC)
|
||||
manifest = cp.validate_package(src)
|
||||
dst = tmp_path / "out"
|
||||
cp.copy_package(src, dst, manifest, "package")
|
||||
assert cp.validate_package(dst)["id"] == "demo/evaluated"
|
||||
|
||||
|
||||
def test_copy_refuses_to_overwrite(tmp_path: Path) -> None:
|
||||
src = write_evaluated(tmp_path / "src", RUBRIC)
|
||||
manifest = cp.validate_package(src)
|
||||
dst = tmp_path / "out"
|
||||
cp.copy_package(src, dst, manifest, "package")
|
||||
with pytest.raises(cp.CannedPromptError, match="already exists"):
|
||||
cp.copy_package(src, dst, manifest, "package")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue