Skip to content

Latest commit

 

History

History
202 lines (147 loc) · 17.5 KB

File metadata and controls

202 lines (147 loc) · 17.5 KB

CLAUDE.md

Guidance for Claude Code working in this repository. Read this together with docs/strategy-unified-vector-quantities.md (the architecture spec for the semantic-quantity system) and docs/physics-generator.md (the metadata workflow).

Build commands

This is a multi-target .NET library using ktsu MSBuild SDKs. Strings and Paths target net8.0net10.0 plus netstandard2.0/netstandard2.1; Quantities and the storage-type alias packages target net8.0net10.0 (they require INumber<T>).

  • Build: dotnet build
  • Test: dotnet test
  • Test (verbose): dotnet test --logger "console;verbosity=detailed"
  • Clean: dotnet clean
  • Restore: dotnet restore
  • Format: dotnet format

Tests use MSTest. Generator output is emitted to Semantics.Quantities/Generated/ (committed) so the project can be inspected without first running the generator.

Reproducing SonarCloud warnings locally

CI analyses this repository with the SonarCloud scanner, which injects the Sonar analyzers into the compilation. A plain dotnet build does not run them, so Sonar findings are invisible locally and only surface after a push — a ~10 minute round trip per attempt. To run the same analyzers:

dotnet build -p:CustomAfterMicrosoftCommonProps=$PWD/.sonarlint/sonar-local.props
dotnet build -p:CustomAfterMicrosoftCommonProps=$PWD\.sonarlint\sonar-local.props

Note After, not Before. CustomBeforeMicrosoftCommonProps reaches only Semantics.SourceGenerators, the one project declaring its SDK with the <Project Sdk="..."> attribute form; the ktsu.Sdk projects use <Project> with <Sdk Name="..." /> elements, which that hook does not reach. CustomAfterMicrosoftCommonProps does reach them, and is still early enough for restore to pick up the analyzer PackageReference.

The opt-in lives in .sonarlint/sonar-local.props (analyzer package) and .sonarlint/sonar-local.globalconfig (rule severities — it raises the rules CI reports that the analyzer package ships disabled, and silences the ones CI's quality profile does not report). Nothing imports these automatically, so normal builds, the CI pipeline, and packaging are unaffected.

Caveat: the globalconfig was calibrated against this repository's quality profile. If a project sits under a different profile the local rule set will not match it exactly, so treat a clean local run as strong evidence rather than proof.

Project layout

Project Responsibility
Semantics.Strings Strongly-typed string wrappers (SemanticString<T>) and validation attributes/strategies.
Semantics.Strings.Identifiers Concrete identifier string types (Uuid, Ulid, Iban, Isbn, CreditCardNumber, JwtToken) built on the Semantics.Strings framework.
Semantics.Paths Polymorphic file system path types (IPath, IFilePath, IDirectoryPath, …).
Semantics.Music Immutable musical value types (Pitch, Interval, Scale, Chord, Key, Duration, TimeSignature) plus an analysis aggregate layer (Progression, Section, Arrangement, Form) computing roman numerals, cadences, key inference, chromatic identification, and named forms. Targets net8.0net10.0 + netstandard2.0/netstandard2.1.
Semantics.Color Physically-grounded color types. Canonical linear-RGB Color hub plus color-space satellites (Srgb, Hsl, Hsv, Oklab, Oklch); every type converts to and from every other, routed through the nearest shared hub (Srgb within the sRGB family, Oklab within the perceptual family, linear Color across families) so no conversion takes a redundant gamma round-trip. Also WCAG accessibility tooling, HSL/perceptual adjustment operations (lighten/saturate/hue/invert), and NamedColors. Targets net8.0net10.0 + netstandard2.0/netstandard2.1.
Semantics.Quantities Hand-written runtime types (PhysicalQuantity<TSelf, T>, IVector0..IVector4, UnitSystem) plus generator output under Generated/.
Semantics.SourceGenerators Roslyn incremental generators that emit quantity types, units, conversions, magnitudes, physical constants, and storage-type helpers from metadata. Only the physics-specific half lives here — Models/, Metadata/, Generators/, and the bindings in SemanticsGenerator/SemanticsDiagnostics/Emit. The C# syntax templates come from ktsu.CodeBlocker.Templates; the metadata-driven generator base, metadata loading and the diagnostic catalogue come from ktsu.SourceGeneratorToolkit (#181, #192).
Semantics.Quantities.{Double,Float,Decimal} Props-only satellite packages. Each ships a buildTransitive props file (generated by scripts/Generate-AliasProps.ps1) that injects global-using aliases binding every quantity to one storage type, so consumers write Mass instead of Mass<double>.
Semantics.Test MSTest project covering all of the above.

Semantic quantities architecture (the unified vector model)

The quantity system is metadata-driven. The single source of truth is Semantics.SourceGenerators/Metadata/dimensions.json, which lists every physical dimension and the vector forms it supports.

Every quantity is a vector. Dimensionality of the direction space is part of the type:

Form Meaning Sign Examples
IVector0<TSelf, T> Magnitude only Always >= 0 Speed, Mass, Energy, Distance, Area
IVector1<TSelf, T> Signed 1D Signed Velocity1D, Force1D, Temperature, ElectricCharge
IVector2<TSelf, T> 2D directional Per-component Velocity2D, Force2D, Acceleration2D
IVector3<TSelf, T> 3D directional Per-component Velocity3D, Force3D, Position3D
IVector4<TSelf, T> 4D directional Per-component (reserved for relativistic / spacetime)

IVectorN.Magnitude() (for N >= 1) returns the corresponding IVector0.

All generated types are generic over a numeric storage type: where T : struct, INumber<T>.

Resolved design decisions

These are now baked into the generator and enforced by tests. Do not reopen without an architecture discussion.

  1. V0 - V0 returns the same V0 of T.Abs(a - b). Magnitude subtraction stays non-negative; signed subtraction must use the V1 form explicitly.
  2. Dimensionless and angular quantities have both Ratio (V0) and SignedRatio (V1) bases. Ratios that semantically must be non-negative (e.g. RefractiveIndex, MachNumber, SpecificGravity) are V0 overloads of Ratio.
  3. Semantic overloads widen implicitly to their base, narrow explicitly from it. A Weight is implicitly a ForceMagnitude; the reverse requires Weight.From(forceMagnitude) or an explicit cast.
  4. Physical constraints are enforced structurally via the V0 (magnitude) form. Vector0 factories run Vector0Guards.EnsureNonNegative and throw ArgumentException on a negative value. That covers absolute zero (Temperature is V0, so Kelvin must be ≥ 0), non-negative frequency, non-negative absolute pressure, etc. A V0 overload can opt into a stricter rule by declaring physicalConstraints: { "minExclusive": "0" } in dimensions.json (#51); the generator then emits Vector0Guards.EnsurePositive and rejects zero too. Used today for Wavelength, Period, and HalfLife — quantities for which zero is unphysical.
  5. Logarithmic-scale quantities are generated from logarithmic.json, not declared as dimensions. Decibel scales (Decibels, SoundPressureLevel, SoundIntensityLevel, SoundPowerLevel, DirectionalityIndex), pitch intervals (Cents, Semitones), and PH don't obey linear arithmetic, so they are emitted by LogarithmicScalesGenerator as standalone readonly partial record structs built around scale = multiplier · log_base(linear / reference), converting to and from their linear generated counterparts (Gain, Ratio, SoundPressure, SoundIntensity, SoundPower, Concentration). Bespoke members (named constants like PH.Neutral, cross-scale conversions like CentsSemitones) live in hand-written partials next to the metadata-generated core. Adding a new log-scale quantity means adding a logarithmic.json entry, plus a partial only if it needs bespoke members.

Physical constants

PhysicalConstants is generated from domains.json. Public surface:

// Domain-grouped generic accessors:
PhysicalConstants.Fundamental.SpeedOfLight<T>()
PhysicalConstants.Fundamental.PlanckConstant<T>()
PhysicalConstants.AngularMechanics.DegreesPerRadian<T>()

// Flat accessors over every constant, regardless of domain:
PhysicalConstants.Generic.SpeedOfLight<T>()
PhysicalConstants.Generic.PlanckConstant<T>()
PhysicalConstants.Generic.DegreesPerRadian<T>()

Each constant is emitted as T.Parse(literal, NumberStyles.Float, CultureInfo.InvariantCulture) into a private Values<T> holder nested in its domain class, so the literal is parsed once per closed generic type and the package needs no arbitrary-precision dependency. NumberStyles.Float is mandatory, not incidental: several literals are in exponent form (6.62607015e-34, 20e-6), and the Parse(string, IFormatProvider) overload resolves to NumberStyles.Number for some numeric types, which rejects an exponent.

Operators and physics relationships

Cross-dimensional relationships are also declared in dimensions.json (integrals, derivatives, dotProducts, crossProducts). The generator emits operators like:

public static Energy<T> operator *(Force1D<T> f, Length<T> d) =>
    Energy<T>.Create(f.Value * d.Value);

All values are stored in SI base units, so operators read .Value directly. Suppress CA2225 on physics operators because "named alternates" (Add, Multiply) don't carry the dimensional meaning:

[System.Diagnostics.CodeAnalysis.SuppressMessage(
    "Usage", "CA2225:Operator overloads have named alternates",
    Justification = "Physics relationship operators represent fundamental equations.")]

Code standards

File headers

// Copyright (c) 2023-2026 ktsu-dev contributors

The text comes from COPYRIGHT.md; ktsu.Sdk syncs .editorconfig's file_header_template from that file on every build, and IDE0073 enforces it. Generated files are written with LF line endings, pinned in CreateCodeBlocker to match * text=auto eol=lf in .gitattributes. If COPYRIGHT.md changes, update GeneratorBase.WriteHeaderTo to match — generator output is committed source, so a drift there shows up as a diff rather than a build error (.g.cs is exempt from IDE0073). SourceGeneratorTests asserts the emitted header, so the drift fails a test instead.

Generator-emitted files additionally carry // <auto-generated />.

Validation and error handling

  • Throw ArgumentException for validation failures (not FormatException).
  • Throw DivideByZeroException when dividing by zero in DivideToStorage.
  • Use the most specific exception type available.

Testing

  • Use explicit types (no var) in test bodies.
  • Pre-create fixtures outside measurement loops in performance tests.
  • Mark OS-specific tests with [TestCategory("OS-Specific")].
  • Use 259-character path limit for cross-platform path tests.
  • Force GC before memory measurements: GC.Collect(); GC.WaitForPendingFinalizers();

XML documentation

  • /// <summary>Gets the physical dimension of <quantity> [<symbol>].</summary> style for dimension properties.
  • Include <param>, <returns>, <exception>, and <see cref=""> tags on public APIs.

Important implementation notes

Semantic string creation

var email = EmailAddress.Create("user@example.com");
var userId = UserId.Create("USER_123");

// Extension method conversion
var email2 = "user@example.com".As<EmailAddress>();

// Cross-type conversion
var converted = sourceString.As<SourceType, TargetType>();

Path conversion

  • AsAbsolute() — convert to absolute using current working directory.
  • AsAbsolute(baseDirectory) — convert to absolute using a specific base.
  • AsRelative(baseDirectory) — convert to relative against a specific base.

Working with the source generator

  • A generator declares the metadata files it reads via MetadataFileNames and derives from SemanticsGenerator<T> (one file) or SemanticsMultiFileGenerator (several). Neither needs to override Initialize. Those two bind this repository's diagnostic catalogue and file header onto GeneratorBase/GeneratorBase<T> from ktsu.SourceGeneratorToolkit; that binding is the seam that keeps the package consumer-agnostic, so keep repository-specific detail on this side of it.
  • Emitted C# is described with the template model from ktsu.CodeBlocker.TemplatesSourceFileTemplate, ClassTemplate, FieldTemplate, MethodTemplate and friends. The model owns punctuation, spacing, member ordering and indentation, so a BodyFactory writes only the body: => Create(value); for an expression body, or a braced block. Do not prefix it with a space; the model supplies the separator.
  • A type's declaration keyword comes from ClassTemplate.Kind (TypeKind.Class, TypeKind.Record, …), not from Keywords, which carries only modifiers. Collection properties are read-only, so use collection-initializer syntax (Keywords = { "public" }) rather than assignment, or the WithComments/WithInterfaces helpers when you already hold a sequence.
  • Edit Semantics.SourceGenerators/Metadata/dimensions.json to add a dimension, vector form, semantic overload, or relationship.
  • Rebuild Semantics.SourceGenerators and the consuming Semantics.Quantities project; emitted files appear in Semantics.Quantities/Generated/Semantics.SourceGenerators/<GeneratorName>/.
  • Treat generator output as committed source. Diff it before commit so accidental regressions are visible.
  • After adding or renaming a quantity, regenerate the storage-type alias props with pwsh scripts/Generate-AliasProps.ps1 (it reads the generated catalogue and rewrites Semantics.Quantities.{Double,Float,Decimal}/buildTransitive/*.props) and commit them. The verify-generated workflow rebuilds, regenerates, and fails the PR if either the generated sources or the alias props drift.
  • Factory names are the singular lemma (#49). The generator emits From{name} using each unit's name from units.json verbatim (e.g. Length.FromMeter, Mass.FromKilogram, Speed.FromMeterPerSecond, Length.FromFoot, Frequency.FromHertz). The rule is purely mechanical, so name must itself be the singular lemma — including compounds, whose leading noun is singular too (MeterPerSecond, RevolutionPerMinute, PartPerMillion, not MeterPerSecond/RevolutionPerMinute/PartPerMillion). There is no factoryName field and no pluralisation step; the generator never has to know English pluralisation.
  • Generator diagnostics:
    • SEM001 — a relationship in dimensions.json references a dimension that does not exist (typo or rename). The operator is silently dropped.
    • SEM002 — schema-level validation issue (missing name/symbol, empty availableUnits, duplicate type names, no vector forms declared).
    • SEM003 — a relationship's explicit forms list references a vector form not declared on a participating dimension. Use forms to constrain a relationship to specific vector forms (e.g. crossProducts: [{ "other": "Length", "result": "Torque", "forms": [3] }]); when omitted, the legacy "emit at every common form" behaviour is preserved.
    • SEM004 — a dimension's availableUnits array references a unit name that isn't declared anywhere in units.json. Without the diagnostic the generator silently emits an identity-conversion From{Unit} factory, which is wrong for any non-base unit; SEM004 catches the typo at build time.
    • SEM005 — schema-level validation issue in logarithmic.json (missing or duplicate scale names, a conversion with no linear type).
    • SEM006 — a metadata file a generator declared in MetadataFileNames was not supplied as an AdditionalFile. Previously this produced no output and no explanation, which is indistinguishable from a generator that simply had nothing to emit.
    • SEM007 — a metadata file could not be parsed. Replaces the base generator's CONV001 in category SourceGenerator, and covers the path that used to swallow the exception, where a malformed units.json silently produced factories with no scale factor.
    • Descriptors are allocated from SemanticsDiagnostics, which is the one place to add a new one. AnalyzerReleaseTrackingTests fails if the identifier is missing from AnalyzerReleases.Unshipped.md, so RS2008 no longer surfaces only after a push.
  • See docs/physics-generator.md for the full schema and an end-to-end "add a dimension" walk-through.

This file is the entry point. For deeper material:

  • docs/strategy-unified-vector-quantities.md — architecture spec for the unified vector model.
  • docs/physics-generator.md — generator + dimensions.json schema.
  • docs/architecture.md — semantic strings/paths/validation architecture (SOLID, design patterns).
  • docs/complete-library-guide.md — user-facing guide to all components.
  • docs/validation-reference.md — list of validation attributes.
  • docs/advanced-usage.md — advanced patterns for strings/paths.
  • docs/migration-guide-2.0.md — 1.x → 2.0 upgrade guide (renames, namespace moves, behavioral changes).
  • docs/migration-guide-3.0.md — 2.x → 3.0 upgrade guide (removed first-class .NET type attributes, chord flag enum renames).
  • docs/migration-guide-3.1.md — 3.0 → 3.1 upgrade guide (JSON converter is now opt-in, PhysicalConstants domain fields became generic accessors).