Skip to content

feat(quantities): per-overload physicalConstraints + EnsurePositive guard (closes #51)#75

Merged
matt-edmondson merged 1 commit intovectorsfrom
claude/issue-51-strict-positive
May 10, 2026
Merged

feat(quantities): per-overload physicalConstraints + EnsurePositive guard (closes #51)#75
matt-edmondson merged 1 commit intovectorsfrom
claude/issue-51-strict-positive

Conversation

@matt-edmondson
Copy link
Copy Markdown
Contributor

Summary

Closes #51.

The V0 non-negativity invariant from #50 already covers most of the constraints listed in the original issue (Temperature ≥ 0 K, Frequency ≥ 0, Pressure absolute ≥ 0). What was still open is the strict-positive case — quantities where zero is unphysical, distinct from zero-allowing magnitudes.

This PR adds the missing schema bit and applies it to the three overloads that need it.

What changed

OverloadDefinition in dimensions.json gains an optional physicalConstraints block:

{ "name": "Wavelength", "description": "...", "physicalConstraints": { "minExclusive": "0" } }

When a V0 overload declares minExclusive: "0", the generator emits its From{Unit} factories with the new Vector0Guards.EnsurePositive helper (rejects zero and negative) instead of the default EnsureNonNegative (rejects negative only). The guard still runs after the unit conversion, so Wavelength.FromNanometers(0) and Wavelength.FromAngstroms(0) both throw — same flow as the #50 / Temperature.FromCelsius(-300) story.

Applied today:

Type Why strict-positive
Wavelength (Length V0 overload) No zero-wavelength wave
Period (Time V0 overload) No zero-period oscillation
HalfLife (Time V0 overload) No zero half-life

Base types (Length, Duration) and other zero-allowing overloads (Distance, Latency, etc.) keep the V0 default — zero stays valid for those.

Spot-checked the regenerated output:

public static Wavelength<T> FromMeters(T value)
    => Create(Vector0Guards.EnsurePositive(value, nameof(value)));

public static Length<T> FromMeters(T value)
    => Create(Vector0Guards.EnsureNonNegative(value, nameof(value)));

Schema scope

PhysicalConstraints only honours minExclusive == "0" today. Future bounds (maxInclusive, non-zero floors, upper bounds, dimension-level constraints) are deliberately not implemented — the issue's open questions called for the strict-positive case specifically. The class shape leaves room to grow (MinExclusive is a string to accommodate future literals) without breaking metadata that opts in today.

Test plan

  • dotnet build Semantics.SourceGenerators and dotnet build Semantics.Quantities clean.
  • Generator emits EnsurePositive for Wavelength/Period/HalfLife and EnsureNonNegative everywhere else (spot-checked).
  • No SEM00x warnings on current metadata.
  • dotnet testSemantics.Test cannot restore in this sandbox (Microsoft.NETCore.App.Host.ubuntu.24.04-x64); CI is the validator. Tests in Vector0InvariantTests.cs cover:
    • Wavelength.FromMeters(0.0) / FromNanometers(0.0) throw
    • Wavelength.FromMeters(550e-9) succeeds (visible-light wavelength)
    • Period.FromSeconds(0.0) / HalfLife.FromSeconds(0.0) throw
    • Length.FromMeters(0.0) / Duration.FromSeconds(0.0) / Distance.FromMeters(0.0) / Latency.FromSeconds(0.0) still succeed
    • Direct Vector0Guards.EnsurePositive zero/negative/positive coverage with paramName propagation.

Doc update

CLAUDE.md "Resolved design decisions" §4 now mentions the overload-level opt-in.

https://claude.ai/code/session_01Tj63Rddvs9frqLUgsjNEP5


Generated by Claude Code

…uard (closes #51)

Adds an optional `physicalConstraints` block to OverloadDefinition in
dimensions.json. When a V0 overload declares `minExclusive: "0"`, its
generated From{Unit} factories use the new
Vector0Guards.EnsurePositive (which rejects zero and negative inputs)
instead of the default EnsureNonNegative (which only rejects negative).

The guard runs after the unit conversion to the SI base unit, mirroring
the existing #50 invariant — so e.g. Wavelength.FromNanometers(0)
correctly throws even though the SI value is exactly zero.

Applied today to:
- Wavelength (Length V0 overload) — no zero-wavelength wave
- Period (Time V0 overload) — no zero-period oscillation
- HalfLife (Time V0 overload) — no zero half-life

The base types (Length, Duration) and other zero-allowing overloads
(Distance, Latency, etc.) keep the V0 default and continue to allow
zero. Verified by spot-checking the generated output:

    public static Wavelength<T> FromMeters(T value)
        => Create(Vector0Guards.EnsurePositive(value, nameof(value)));
    public static Length<T> FromMeters(T value)
        => Create(Vector0Guards.EnsureNonNegative(value, nameof(value)));

Tests added in Vector0InvariantTests.cs cover zero/positive/negative
across Wavelength/Period/HalfLife and the unconstrained Length/Duration/
Distance/Latency baselines, plus direct EnsurePositive helper coverage.

CLAUDE.md "Resolved design decisions" §4 updated to mention the
overload-level opt-in. Other dimensions/overloads that need stricter
bounds in the future can extend the PhysicalConstraints class — the
generator currently honours minExclusive == "0"; future fields
(maxInclusive, dimension-level constraints, etc.) would need
additional generator support.
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
1 Security Hotspot
0.0% Coverage on New Code (required ≥ 80%)
C Reliability Rating on New Code (required ≥ A)
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@matt-edmondson matt-edmondson merged commit d81c5e0 into vectors May 10, 2026
4 of 5 checks passed
@matt-edmondson matt-edmondson deleted the claude/issue-51-strict-positive branch May 10, 2026 13:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants