Robware Software by Rob

Welcome

Hello, I'm Rob. I'm a senior software engineer at On The Beach. Professionally my focus has been in backend development primarily using C#. In my spare time I spend my time riding bikes or making stuff, typically involving an Arduino.

This website is primarily an outlet for me to write about things which have been technically challenging, either in a professional or personal capacity, though not limited to that.

If you wish to get in contact, then get in touch via my LinkedIn profile.

Latest code commit

RepositoryPlaceMark
Branchmain
SHA118e1bd79002764690b025af12df5d7315c216d3
MessageInvariantGlobalization alongside per-site culture guards (#164) Vikunja task 223. Production code correctly used CultureInfo.InvariantCulture, but the tests that appeared to guard it never changed CultureInfo.CurrentCulture — so they passed identically whether the invariant handling existed or not. They could not fail for the reason they existed, which is the failure mode PR #154 caught for one type and this ticket swept for across the rest. Each site is now guarded by a test watched to fail. Under a forced fr-FR with the invariant provider removed, both import parsers rejected the valid coordinates 51.5045 and -0.0865 as "Latitude '51.5045' is not a number." A fourth site beyond the ticket's three was found in Account.razor's markup, which rendered "Linked 1 août 2026" instead of "1 August 2026". Account.razor.cs's two round-trip "O" calls got no test, because round-tripping was confirmed culture-independent across five cultures including two non-Gregorian calendars — no guard there could discriminate anything. InvariantGlobalization was taken as well, not instead: the flag makes the whole class of defect impossible for the shipped application, while the per-site tests keep proving the intent for anyone who later removes it. ADR-0139 records the trade-off. The claim that would have made all of this theatre — that the tests still redden with the flag set — was verified rather than asserted. Review diffed both projects' runtimeconfig.json and confirmed System.Globalization.Invariant lands on PlaceMark.WebUI but not on PlaceMark.WebUI.Tests, since the flag is a property of the executing apphost rather than something a referenced library propagates, then mutated the parser with the flag set and watched the guard redden anyway. It also independently reran the five-culture round-trip check and reproduced both reported corrupted values exactly. UnderCulture is promoted to shared test support, its own doc comment corrected, and CultureInfo.CurrentCulture re-verified as per-logical-call-context from its new home. No production behaviour changed anywhere except the csproj flag. Merged with an admin override: branch protection requires one approving review, which Forgejo will not accept on a self-authored pull request.
Timestamp18:19:21 on Friday the 14th of August 2026

Latest Blog Post

Expanding local LLM capabilities by creating skills from documentation

I've been using OpenCode lately with locally running LLMs with LM Studio, mostly with the Qwen 3.6 35B A3B model. Compared to the likes of Claude, these local models have some significant limitations. One major limitation I was finding with Qwen is that it would always generate FluentAssertions syntax for unit tests despite having Shouldly specified. It seems to understand I want to use Shoudly, but would get stuck generating FluentAssertions syntax. Quite annoying. In order to get around this I decided I'd create a custom skill from the documentation, and then get my agent to explicitly load it.

The steps are pretty basic:

  1. Grab a copy of the git repo
  2. Navigate to the documentation folder
  3. Using OpenCode's own Build agent, tell your LLM to create a skill from the documentation

It really is that simple. I did the same thing with NSubstitute.

~/.config/opencode/skills/shouldly$ tree
.
├── references
│   ├── completeIn.md
│   ├── configuration.md
│   ├── dictionary
│   │   ├── containKeyAndValue.md
│   │   ├── containKey.md
│   │   └── README.md
│   ├── dynamicShould.md
│   ├── enumerable
│   │   ├── allBe.md
│   │   ├── contain.md
│   │   ├── empty.md
│   │   ├── have.md
│   │   ├── oneOf.md
│   │   ├── README.md
│   │   ├── shouldBe.md
│   │   ├── subsetOf.md
│   │   └── unique.md
│   ├── equality
│   │   ├── assignableTo.md
│   │   ├── exampleClasses.md
│   │   ├── greaterLessThan.md
│   │   ├── haveFlag.md
│   │   ├── inRange.md
│   │   ├── matchApproved.md
│   │   ├── notBe.md
│   │   ├── null.md
│   │   ├── ofType.md
│   │   ├── oneOf.md
│   │   ├── README.md
│   │   ├── sameAs.md
│   │   ├── shouldBe.md
│   │   ├── ShouldMatchApprovedChanged.png
│   │   ├── ShouldMatchApprovedInitial.png
│   │   └── trueFalse.md
│   ├── exceptions
│   │   ├── notThrow.md
│   │   ├── README.md
│   │   └── throw.md
│   ├── getting-started.md
│   ├── satisfyAllConditions.md
│   ├── string
│   │   ├── contain.md
│   │   ├── endWith.md
│   │   ├── match.md
│   │   ├── null.md
│   │   ├── README.md
│   │   ├── shouldBe.md
│   │   └── startWith.md
│   └── upgrade
│       └── 3to4.md
└── SKILL.md

I also recommend creating an agent to specialise in software development and explicitly requesting the skill(s) in the agent file's frontmatter:

skills: csharp-developer, shouldly, nsubstitute

I also got it to create documentation on the Seeed Studio XIAO ESP32C3 by telling it to turn the getting started page in to a skill, and also the CAN Bus expansion board, to help with my campervan control system project.

Doing this has significantly improved the accuracy of the output and reduced the number of times it gets stuck in a loop and also reduced the amount of time spent thinking.

Posted on Friday the 10th of July 2026

View more