Skip to content

Adopt ktsu.Semantics for paths and colours - #69

Merged
matt-edmondson merged 3 commits into
mainfrom
explore/semantics-adoption
Aug 5, 2026
Merged

Adopt ktsu.Semantics for paths and colours#69
matt-edmondson merged 3 commits into
mainfrom
explore/semantics-adoption

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Summary

Replaces raw path strings and System.Drawing colour handling with the ktsu.Semantics types, per the ktsu convention of preferring semantic types for strings and paths. Also clears the two SonarCloud smells left over from #66.

Warning

This narrows the accepted colour names. See the breaking change section below.

Paths

Arguments.TryResolveInput and TryResolveOutput resolve the raw option strings into AbsoluteDirectoryPath, normalising relative values against the working directory first, because the semantic type only accepts absolute paths. Validate calls both, which turns two crashes into clean exit 1 errors:

Input Before After
--input that does not exist unhandled DirectoryNotFoundException, stack trace --input directory does not exist: ..., exit 1
--input that is a file unhandled IOException, stack trace --input is a file, not a directory: ..., exit 1

Both previously escaped the per file catch because Directory.GetFiles runs outside it, so they crashed the process and undermined the exit codes added in #66.

Output file paths are now composed with the / operator over FileName rather than Path.Join, which also rejects a name carrying a directory separator.

Colours

ColorParser.TryParse accepts a NamedColors name or a hex value, and System.Drawing is gone. This adds #RGB shorthand and #RRGGBBAA alpha support, and an unparseable colour is caught by validation rather than throwing part way through a run.

Color stores linear channels as doubles, so ProcessImage encodes to sRGB bytes once up front rather than per pixel.

Breaking change

The named colour set narrows from the roughly 140 CSS names System.Drawing.ColorTranslator understood to the 13 in NamedColors: black, white, red, green, blue, yellow, cyan, magenta, gray, grey, orange, purple, transparent.

Anything else must now be given as hex. CornflowerBlue was previously documented in the README and no longer works. Pinned by ColorParserTests.RejectsCssNamesOutsideTheKnownSet and the README is updated.

Sonar smells from #66

  • S3626, redundant jump. The continue at the end of the per file catch was the last statement in the loop body.
  • S3776, cognitive complexity 25 against a limit of 15 in ProcessImage. The pixel passes are now named methods, FindBrightestOpaqueValue, TintAndMeasureBounds and CropSquareAndResize, leaving ProcessImage a readable sequence with one branch.

The inclusive bounding box is now a PixelBounds record struct rather than four loose locals. Its Width and Height carry the +1 the inclusive indices require, which is the off-by-one fixed in #66, and IsEmpty names the inverted bounds check.

Supporting changes

Polyfill moves from a 9.7.1 VersionOverride to 11.0.1 centrally, which is the floor ktsu.Semantics.Strings requires, and that package is referenced directly because SemanticString.Create lives there and the ktsu analyzer rejects transitive use.

Test plan

  • dotnet build -c Release clean, 0 warnings with warnings as errors enabled
  • dotnet test -c Release passes 65 of 65, up from 48. 17 new tests cover colour parsing and the path validation rules
  • Every gold master image is unchanged. Before swapping the parser I verified FromHex().ToBytes() round trips byte for byte across all 256 greyscale values and matches ColorTranslator.FromHtml on sampled values, so the colour change is provably a no-op on output. The gold masters passing unchanged confirms it, and also confirms the ProcessImage refactor is behaviour preserving
  • All five validation paths checked against the real binary, covering both former crashes, the unknown colour name, #F80 shorthand and the orange named colour

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ruh9nAQiuU5VGHV8uJCXtu

Replaces raw path strings and System.Drawing colour handling with the
ktsu.Semantics types, per the ktsu convention of preferring semantic types for
strings and paths.

Paths: Arguments.TryResolveInput and TryResolveOutput resolve the raw option
strings into AbsoluteDirectoryPath, normalising relative values against the
working directory first because the semantic type only accepts absolute paths.
Validate calls both, which turns two crashes into clean exit 1 errors:

- an --input directory that does not exist previously threw an unhandled
  DirectoryNotFoundException from Directory.GetFiles
- an --input that is a file rather than a directory previously threw an
  unhandled IOException

Output file paths are now composed with the / operator over FileName rather
than Path.Join, which also rejects a name carrying a directory separator.

Colours: ColorParser.TryParse accepts a NamedColors name or a hex value, and
System.Drawing is gone. This adds #RGB shorthand and #RRGGBBAA alpha support,
and an unparseable colour is now caught by validation instead of throwing part
way through a run.

BREAKING: the named colour set narrows from the roughly 140 CSS names that
System.Drawing.ColorTranslator understood to the 13 in ktsu.Semantics
NamedColors. Any other colour must now be given as hex. Pinned by
ColorParserTests.RejectsCssNamesOutsideTheKnownSet.

Semantics.Color stores linear channels as doubles, so ProcessImage encodes to
sRGB bytes once up front rather than per pixel. FromHex().ToBytes() round trips
byte for byte, verified across all 256 greyscale values before the swap, which
is why every gold master image is unchanged.

Supporting changes: Polyfill moves from a 9.7.1 VersionOverride to 11.0.1
centrally, which is the floor ktsu.Semantics.Strings requires, and that package
is referenced directly because SemanticString.Create lives there and the ktsu
analyzer rejects transitive use.

Adds 17 tests covering colour parsing and the new path validation rules.

Claude-Session: https://claude.ai/code/session_01Ruh9nAQiuU5VGHV8uJCXtu
S3626, redundant jump. The continue at the end of the per file catch block was
the last statement in the loop body, so it did nothing. The guard continue that
skips .new.png files earlier in the loop is a real one and stays.

S3776, cognitive complexity 25 against a limit of 15 in ProcessImage. The method
carried the whole pipeline inline, including two nested pixel loops with their
own branching. The passes are now named methods, FindBrightestOpaqueValue,
TintAndMeasureBounds and CropSquareAndResize, leaving ProcessImage as a
readable sequence of steps with a single branch for the blank image case.

The inclusive bounding box is now a PixelBounds record struct rather than four
loose locals threaded through the method. Its Width and Height carry the +1 that
the inclusive indices require, which is the off-by-one fixed earlier, and IsEmpty
names the inverted bounds check that detects an image with nothing opaque in it.

Behaviour is unchanged. All 65 tests pass, including the gold master cases,
which compare output pixel for pixel.

Claude-Session: https://claude.ai/code/session_01Ruh9nAQiuU5VGHV8uJCXtu
SonarCloud reported 12 issues on the new test files, all MSTEST0037 and
MSTEST0046 suggesting the newer assertion methods.

- Assert.AreEqual(n, errors.Count) becomes Assert.HasCount(n, errors)
- Assert.AreEqual(0, errors.Count) becomes Assert.IsEmpty(errors)
- StringAssert.Contains becomes Assert.Contains

The last one is not a rename. StringAssert.Contains takes the haystack first,
Assert.Contains takes the needle first, so the arguments had to swap. Both
parameters are strings, so getting that backwards would still compile. It would
not pass, because the assertion would then be checking whether a long error
message is a substring of a short literal, and the suite passing is what
confirms the order is right.

Also converted the two equivalent AreNotEqual checks in GoldMasterTests, which
the analyzer raised back when the test suite landed and which were never
cleared. They were not in this run's report because that file is unchanged here,
so they no longer count as new code.

All 65 tests still pass.

Claude-Session: https://claude.ai/code/session_01Ruh9nAQiuU5VGHV8uJCXtu
@sonarqubecloud

sonarqubecloud Bot commented Aug 5, 2026

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 4228b64 into main Aug 5, 2026
5 checks passed
@matt-edmondson
matt-edmondson deleted the explore/semantics-adoption branch August 5, 2026 10:37
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.

1 participant