Skip to content

[DV-15] [P2] Parse update ranges strictly and enforce zero-major caret compatibility #575

Closed

Description

@wsdt

Priority: P2
Area: Updater resolver
Repository / target branch: wavect/semaprax / wavect/v090
Reviewed commit: 5428b0805fa3cf35859c843a53e30c3252ef378c
Review date: 6 October 2026

Failure and impact

The update resolver accepts malformed range comparators and expands a zero-major caret range too broadly. An explicit channel range:^0.0.1 selects release v0.0.9 when both v0.0.1 and v0.0.9 are available. Under the conventional caret compatibility rule, the upper bound is <0.0.2 for that fully specified range.

The same parser accepts range:>=1.2.3.4, range:>=1.bad.3, and range:>=1.2.bad. Invalid components silently become zero, while trailing components are ignored. A mistyped constraint therefore becomes a different valid update policy instead of being rejected.

This affects revision selection for both skill and adapter sources. It does not by itself bypass adapter approval or artifact validation; compatible skill content may be eligible for automatic activation under the already approved policy. The defect is that the resolver does not enforce the operator's written constraint reliably.

Exact source and expected semantics

updates/resolve.rs:89–125 parses optional numeric fields through Option and then calls unwrap_or(0). That conflates omitted fields with malformed fields. It also never checks whether a fourth field remains. The caret branch uses minor + 1 whenever major is zero, including 0.0.patch.

The documented channel contract is the highest stable release satisfying the supplied comparator: HARNESS-UPDATES-V1.md:21–30. The ordinary caret convention advances the leftmost nonzero component; Cargo's primary documentation and examples state this behavior. Even if the project chooses a deliberately narrower range grammar, malformed numeric text and extra components must not silently turn into another policy.

Executed reproduction

The probe links directly against the unchanged semaprax_harness library. It registers two synthetic releases with the production MemoryFetcher, then calls the public Channel::parse and resolve::resolve APIs:

versions.add_release(REPO, "v0.0.1", &"1".repeat(40), false);
versions.add_release(REPO, "v0.0.9", &"9".repeat(40), false);
let channel = Channel::parse("range:^0.0.1").unwrap();
let selected = resolve::resolve(&versions, REPO, &channel, "main").unwrap();

Observed:

caret_range=^0.0.1 selected_version=v0.0.9
malformed_range=range:>=1.2.3.4 accepted=true
malformed_range=range:>=1.bad.3 accepted=true
malformed_range=range:>=1.2.bad accepted=true

See repros/updates/repro_updates.rs and evidence/updates-evidence.txt. This was a public-API test with a deterministic in-memory upstream, with no remote release or installation modified. The reproduction asserts the current undesired behavior; it must be converted to expected rejection/selection in the owning tests.

Focused fix

Use a well-tested SemVer range implementation already suitable for the project, or make this small parser strict and completely consume its input. Distinguish an absent numeric component from an invalid one, define the supported partial-version syntax, and implement caret/tilde bounds consistently with the documented grammar. Check bound arithmetic so extremely large valid numeric components cannot panic or wrap when computing an exclusive upper bound.

Keep the existing explicitly supported comma-separated conjunction grammar, stable-release filtering, pinned commits, head resolution, and moved-tag detection. This is not a request to add every npm or Cargo range feature. If an external parser supports a wider grammar, explicitly admit only what Semaprax intends to support and document it.

Acceptance criteria

  • ^0.0.1 accepts 0.0.1 and refuses 0.0.2/0.0.9; resolution picks the highest actually satisfying release.
  • ^0.2.3 stays below 0.3.0, and ^1.2.3 stays below 2.0.0.
  • Invalid numeric text, empty required fields, extra components, and unsupported suffixes return a bounded SPX-HPU001 diagnostic.
  • Invalid input is rejected before upstream requests or update-state mutation.
  • Document and test partial versions and tilde bounds; preserve supported comparator conjunctions.
  • Numeric boundary cases cannot overflow in debug or release builds.
  • Prerelease/draft exclusion and exact-commit/head channels continue to satisfy their existing contracts.

Existing issue boundary and implementation handoff

#442 (HN-05) is the feature predecessor. The retrieved backlog contained no issue describing these parser failures. Read AGENTS.md and HARNESS-UPDATES-V1.md; extend the existing resolver/update tests with direct parsing and actual highest-release resolution cases. Keep this separate from transaction serialization: a single-threaded resolver reproduces the bug.

Activity

  1. wsdt commented on Oct 6, 2026

    @wsdt
    MemberAuthor

    Fixed in a028b4d (pushed to main). Range components are parsed strictly (non-empty digits, no sign/leading zero, fits u64, full consumption) with SPX-HPU001 before any fetch or state write; caret bounds follow the zero-major rule (^0.0.1 <0.0.2, ^0.2.3 <0.3.0, ^1.2.3 <2.0.0, ^0.0 <0.1.0, ^0 <1.0.0), tilde bounds documented; overflowing bounds rejected instead of wrapping. Grammar documented in HARNESS-UPDATES-V1. Evidence: resolve.rs unit tests over every bound and 21 malformed inputs, plus hp_dv15_* integration tests proving bad ranges make no upstream request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions