Skip to content
learn

Journal

  • Course code: file-based apps, rejected snippets and exercise solutions, compared with their expected output by check.sh
  • CI on Linux, Windows and macOS
  • Lesson 1: install .NET and run your first program
  • Lesson 2: variables, types and input
  • Lesson 3: conditions and loops
  • Lesson 4: methods, arrays and lists
  • Lesson 5: classes and objects
  • Lesson 6: records, structs and enums
  • Lesson 7: interfaces and inheritance
  • Lesson 8: exceptions and null safety
  • Lesson 9: collections and LINQ
  • Lesson 10: files and text
  • Lesson 11: unit tests
  • Lesson 12: a small project
Expected What happens Where Measurement Status
Printing a GA chord template shows its Name, as the base record’s ToString() => Name intends The derived records TonalModal and Analytical each synthesize their own ToString, which lists every property: TonalModal { Name = Major 7th, PitchClassSet = 0 4 7 E, … }. A derived record keeps the base’s ToString only when it is sealed (records, built-in formatting) ChordTemplate.cs:84; one caller, ContextualChordsController.cs:301, passes template.ToString() to a service that logs it (FretboardServices.cs:536) A console probe referencing GA.Domain.Core at 5c3a52a, SDK 10.0.112: the dump for ToString() and $"{template}" on both records; a replica with sealed printed maj7 (entry) Reproduced; fix proposed in GA PR #816, not merged yet (2026-10-04); still open on 2026-10-06, with the defect unchanged at main 40d33747 (2026-10-06)
Nullable warnings show in GA’s code: GA.Domain.Core and GA.Core set <Nullable>enable</Nullable> Directory.Build.props puts 15 nullable warnings (CS8600, CS8602, CS8618, CS8625, …) in NoWarn “to achieve a clean baseline”, and Directory.Build.targets repeats them. The baseline is clean without them, so the lists only hide new mistakes Directory.Build.props:21, Directory.Build.targets:7 A file with seven nullable mistakes, added to GA.Domain.Core at 5c3a52a: 0 warnings with GA’s configuration, 7 with both lists removed; without that file, 0 in GA.Domain.Core and GA.Core (entry, ga-probes) Reproduced; not reported to GA
GA’s music knowledge services load ChordProgressions.yaml, GuitarTechniques.yaml and SpecializedTunings.yaml All three fail to deserialize: one string where the class has a List<string>, strings where it expects objects, a line that isn’t valid YAML. Each loader catches the exception, prints Error loading … and carries on with one default item ChordProgressions.yaml:93 against ChordProgressionsConfigLoader.cs:16; GuitarTechniques.yaml:23 against GuitarTechniquesConfigLoader.cs:20; SpecializedTunings.yaml:95; the catch of ChordProgressionsConfigLoader.cs:137, and the same in the two other loaders artists.cs at 5c3a52a, SDK 10.0.112: 17 iconic chords, then 1 item for each of the three files, and 16 distinct artists (entry). The three lines are unchanged at GA’s main ae196bf Reproduced; reported as GA issue #797 on 2026-10-03; fix proposed in GA PR #808, not merged yet (2026-10-04)
GetArtistBreakdown returns the 20 artists with the most entries, the most first, as its comment // Top 20 artists says Take(20) cuts the alphabetical list before the sort by count, and the sorted result goes into a Dictionary, whose order the documentation leaves undefined MusicalKnowledgeService.cs:226 and :235; GetAllArtists sorts by name at :125 The same probe: with 16 artists, none is left out, and the dictionary comes out by count, ties in alphabetical order. The cut only bites past 20 artists, once the row above is fixed Latent, read in the code; fix proposed in GA PR #811, not merged yet (2026-10-04)
PitchClassSet.ScaleVideoUrl gives the video URL of a scale listed in scale_video_urls.json, which holds 1,490 The file is neither embedded in the library nor copied next to it: GA.Domain.Core.csproj has no rule for it. The resource name GA.Domain.Core.Scales.Data.scale_video_urls.json and the fallback path Scales/Data/ both miss its folder, Theory/Tonal/Scales/Data/, and LoadVideoUrls returns an empty dictionary without a message ScaleVideoUrlById.cs:30, :36-38; PitchClassSet.cs:350, ScaleMetadataRegistry.cs:36 files.cs at 5c3a52a, SDK 10.0.112: 0 resources in GA.Domain.Core, no fallback file, 0 scales with a URL (entry). The resource name and the fallback path are unchanged at GA’s main on 2026-10-03 Reproduced; fix proposed in GA PR #817, not merged yet (2026-10-04); still open on 2026-10-06, with the defect unchanged at main 40d33747 (2026-10-06)
IconicChordsConfigLoader.ReloadConfiguration() reads IconicChords.yaml again after a change It sets the static readonly field _configuration with FieldInfo.SetValue, which throws FieldAccessException since .NET Core 3.0 (breaking change) IconicChordsConfigLoader.cs:107-111, :12 files.cs: FieldAccessException: Cannot set initonly static field '_configuration' after type 'GA.Domain.Services.IconicChordsConfigLoader' is initialized. (entry). Unchanged at GA’s main on 2026-10-03 Reproduced; fix proposed in GA PR #811, not merged yet (2026-10-04)
A missing IconicChords.yaml is reported FindYamlFile tries four paths, two of them relative to the current directory. When none exists, an empty catch swallows its FileNotFoundException, and the loader returns one default chord without a message IconicChordsConfigLoader.cs:94-104, :25-28 files.cs, after deleting the YAML files next to it: 1 chord, “C Major Triad”, started from ga-probes; 17 from the root of GA’s code, through <current directory>/Common/GA.Business.Config; nothing printed (entry) Reproduced; a design choice, not reported to GA
GaCLI reads the appsettings.yaml that its build copies next to the program SetBasePath(Directory.GetCurrentDirectory()) reads it from the current directory, as an optional file: started from another folder, GaCLI would run without its settings, silently Program.cs:17-19, GaCLI.csproj:59-61 Built at 5c3a52a and started from another folder: it prints its list of commands. The file’s two sections, EmbeddingService and LmStudio, are read in one place only, sync-mongodb, into settings that no service resolves (entry) Verified: the file changes nothing at this commit; not reported to GA
Each row of GA’s naturalness CSV has the 6 values of its header :F2 and {deltaStretch} use the machine’s culture: under fr-FR, 2.50 is written 2,50, and a row gets 8 values; the reader splits on , NaturalnessTrainingDataGenerator.cs:160, :21; TrainNaturalnessModelCommand.cs:51-54 l10_culture.cs with GA’s format string: 6 values under en-CA, 8 under fr-FR. GA’s generator itself not run: it needs a corpus of tablatures (entry) Reproduced with the format string; fix proposed in GA PR #811, not merged yet (2026-10-04)
GA’s command prints the number of data rows it wrote csvContent.Split('\n').Length - 1 removes the empty item after the last line break but counts the header: one row too many GenerateNaturalnessDataCommand.cs:38 The arithmetic of Split('\n'), measured by l10_line_endings.cs: 3 lines give 4 items. GA’s command not run Latent, read in the code; fix proposed in GA PR #811, not merged yet (2026-10-04)
A test of GA’s music knowledge services fails when their YAML files don’t load Three of the eight tests are skipped with [Ignore("Configuration files not loaded in test environment")]. Of the five others, three only check that a count is greater than zero or that a list isn’t empty, which the one default item of each failed loader satisfies MusicalKnowledgeServiceTests.cs:108-110, :141-143, :207-209; :16, :31-33, :179-183 Probe knowledge-tests, GA’s test file compiled unchanged, SDK 10.0.112: 5 passed, 3 skipped, 0 failed, while three of the four YAML files don’t load. The search for “jazz” passed on 1 result, the default chord progression (entry). Unchanged at GA’s main, 0843879 Reproduced; related to GA issue #797; fix proposed in GA PR #808, not merged yet (2026-10-04)
GA’s [SetUpFixture] TestEnvironment runs before the configuration tests: it sets GA_TEST_MODE=1 and moves the current directory to the repository root It is alone in the namespace GA.Business.Core.Tests.TestBootstrap, and NUnit runs a setup fixture only for the tests of its namespace and the namespaces inside it (NUnit): it never runs TestEnvironment.cs:1, :7-14 Probe knowledge-tests: a test in GA.Business.Core.Tests.Configuration printed GA_TEST_MODE: (not set) and Current directory is the test's bin folder: True (entry). At main, 0843879, the namespace still holds only this file Reproduced; fix proposed in GA PR #816, not merged yet (2026-10-04); still open on 2026-10-06, with the defect unchanged at main 40d33747 (2026-10-06)
The test files of GA.Business.Core.Tests are compiled and run The project file removes 35 files from the build, “Exclude test files with missing service implementations”: 5 of the names match no file, and the other 30 hold 333 lines that start with [Test] or [TestCase] GA.Business.Core.Tests.csproj:39-75 git ls-tree and a count at 5c3a52a; the project has 136 .cs files. The 35 entries are unchanged at main, 0843879 Counted in the code; not reported to GA
GA’s CI runs the tests of every test project Its jobs build and test AllProjects.slnx, which lists 75 of the 111 project files. Six test projects are outside it and named by no workflow; their files hold 102 lines with a test attribute. One of them, GA.Business.Core.Graphiti.Tests, doesn’t compile: it uses GA.Business.Graphiti and references no project AllProjects.slnx, ci.yml:43, :103, GA.Business.Core.Graphiti.Tests.csproj lesson12_counts.py at 5c3a52a; the build of the Graphiti tests stops with 5 errors, CS0234 on its two using GA.Business.Graphiti... lines and CS0246 Counted and built; fix proposed in GA PR #812, not merged yet (2026-10-04)
The PackageReference Update items of GA’s Directory.Build.props align package versions, under “Centralized package version alignment and vulnerability remediation” Directory.Build.props is imported before the project’s own items, so Update finds nothing to update. 23 declarations of the six packages concerned keep another version, 13 of them in projects of the solution Directory.Build.props:32-58 Restore of GA.Business.Core.Graphiti.Tests: Microsoft.Extensions.Http 9.0.10, as its project file asks. With the same Update line in Directory.Build.targets: 10.0.0 Reproduced, with a control; fix proposed in GA PR #812, not merged yet (2026-10-04)
GA’s security scan job reports vulnerable packages Directory.Build.props and Directory.Build.targets turn off NU1902, NU1903 and NU1904 for every project. The scan step has continue-on-error: true, and dotnet list package --vulnerable exits with 0 when it lists a vulnerable package: the job can’t fail Directory.Build.props:16-17, Directory.Build.targets:5-11, ci.yml:398-401 Exit code 0 with a vulnerable package, measured on a scratch project (Experiments table); GA’s scan not run Read in the code; fix proposed in GA PR #812, not merged yet (2026-10-04)
GA’s Quality Check workflow runs a quality benchmark of GaCLI benchmark-quality throws NotImplementedException, as 12 of GaCLI’s 28 commands do, analyze-chord, the first example of its help, included. The step has continue-on-error: true quality_check.yml:26-29, Program.cs:179-182 Local build at 5c3a52a: unhandled NotImplementedException. GA’s run 37167745677, on a later commit whose workflow and Program.cs are the same files: exit code 134, run conclusion success Reproduced; not reported to GA
A GaCLI command that fails tells its caller sync-mongodb catches its exception, prints Error during sync: Unable to resolve service for type 'GA.Data.MongoDB.Services.MusicalObjectsService' while attempting to activate 'GaCLI.Commands.Runner'. and exits with 0: Runner needs a service that the command doesn’t register Program.cs:356, :364-368 Run from bin/Debug/net10.0 and from another folder: the same message, exit code 0 Reproduced; fix proposed in GA PR #811, not merged yet (2026-10-04)
Question Hypothesis, written before measuring Result Verdict Entry and code
How many nullable warnings does GA’s NoWarn hide in GA.Domain.Core? More than 100 0 in GA.Domain.Core and GA.Core with both lists removed; a test file with seven mistakes gets 7 warnings without the lists, 0 with them Refuted entry, ZzNullableCanary.cs
What does GA’s Fret do with 50? new Fret(50), Fret.FromValue(50) and Fret f = 50; throw an ArgumentOutOfRangeException; Fret.TryCreate(50) returns the failure Fret number must be between -1 (muted) and 36, got 50 3 of 3 throw, for 50 and for -2; TryCreate’s message matches word for word Confirmed entry, probe.cs, results.txt
Does new PositionLocation(5, 3), the string and the fret swapped as plain numbers, compile silently? Both orders compile without a warning; the swapped call builds string 5, fret 3 0 warnings on the probe’s lines; it prints 5,3 Confirmed entry, probe.cs
In what order does a foreach read a dictionary after a Remove and an Add? Keys C, D, E, G, A, then Remove("D") and Add("F#"): F# takes D’s place, C F# E G A, not the last place C F# E G A Confirmed entry, l09_dictionary_order.cs
Does changing a dictionary inside its own foreach throw, as it does for a list? Remove doesn’t throw on .NET 10; adding a key throws; List<T>.Remove throws an InvalidOperationException Remove leaves C E without an exception; adding B, and the list’s Remove, both throw Collection was modified; enumeration operation may not execute. Confirmed entry, l09_modify_dictionary.cs, l09_modify_while_looping.cs
Which artists does GA’s GetArtistBreakdown leave out with Take(20)? More than 20 distinct artists: the first 20 of the alphabet are kept, and at least one artist left out has more entries than the smallest count kept; the dictionary comes out by descending count 16 artists, none left out, because three of the four YAML files don’t load (QA table). The dictionary does come out by count, ties in alphabetical order Refuted entry, artists.cs, artists-results.txt
On a 3-line file that ends with a line break, what do ReadAllText(path).Split('\n') and ReadAllLines return, with LF and with CRLF? Split gives 4 items, the last one empty, and with CRLF the first three end with \r; ReadAllLines gives 3 lines without \r for both Lengths LF 2 2 2 0 and 2 2 2, CRLF 3 3 3 0 and 2 2 2; with CRLF, split[0] == "E2" is False Confirmed entry, l10_line_endings.cs
How many bytes do WriteAllText(path, "Ré") and WriteAllText(path, "Ré", Encoding.UTF8) write? 3 without an encoding, 6 with Encoding.UTF8, starting EF BB BF; ReadAllText returns Ré for both 52C3A9, then EFBBBF52C3A9; Ré, 2 characters Confirmed entry, l10_bom.cs
Does File.Exists("data/ga-projects.csv") depend on the folder dotnet run is started from? True from code/csharp-beginner, False from the repository root; a path built from EntryPointFileDirectoryPath exists from both As predicted, and False from examples too; check.sh now runs both cases on three OSes. EntryPointFileDirectoryPath itself was seen before the hypothesis Confirmed entry, l10_where.cs
With the YAML files deleted next to the probe, how many iconic chords does GA load, started from GA’s root and from ga-probes? 17 from GA’s root, through the third path; 1 default chord from ga-probes, with no exception and no message 17, “Hendrix Chord” first, and 1, “C Major Triad”; nothing printed Confirmed entry, files.cs, files-without-yaml.txt, files-from-ga-root.txt
What does GA’s IconicChordsConfigLoader.ReloadConfiguration() do on .NET 10? It throws FieldAccessException: SetValue on a static readonly field is refused since .NET Core 3.0 FieldAccessException: Cannot set initonly static field '_configuration' … Confirmed entry, files.cs, files-results.txt
How many comma-separated values does a row of GA’s naturalness CSV have under en-CA and under fr-FR? 6 under en-CA, 1,2.50,2.50,2,1,4; 8 under fr-FR, 1,2,50,2,50,2,1,4. Seen before the hypothesis: $"{2.5}" gives 2,5 under fr-FR These two rows, exactly Confirmed entry, l10_culture.cs
How long is a file after StreamWriter.Write("E2 A2 D3"), before and after Dispose? 0 bytes before, 8 after 0, then 8 Confirmed entry, l10_dispose.cs
Does GA’s ScaleVideoUrlById find its data? No resource named …scale_video_urls.json in GA.Domain.Core, no fallback file next to the probe, an empty result, no exception. A subagent’s reading suggested it before I read the code 0 resources at all, no fallback file, 0 scales with a URL Confirmed entry, files.cs
What does dotnet new xunit create with SDK 10.0.112, and what does dotnet test print? xUnit v2, package xunit 2.9.x, with xunit.runner.visualstudio, Microsoft.NET.Test.Sdk and coverlet.collector; run by VSTest, ending with a Passed! line xunit 2.9.3, xunit.runner.visualstudio 3.1.4, Microsoft.NET.Test.Sdk 17.14.1, coverlet.collector 6.0.4; VSTest 18.0.2; Passed! - Failed: 0, Passed: 1, … Confirmed entry, Fretboard.Tests.csproj
Does a [Theory] with four [InlineData] rows count as 4 tests or 1? 4 15 tests in the project: with the theory counted once, it would have been 12 Confirmed entry, GuitarTests.cs
What do Assert.Equal(0.3, 0.1 + 0.2) and Assert.Equal(0.3, 0.1 + 0.2, 10) do? The first fails and shows the actual value as 0.30000000000000004; the second passes As predicted. Not predicted: the expected value is shown as 0.29999999999999999 Confirmed entry, PitfallTests.cs
When the method throws ArgumentOutOfRangeException, what do Assert.Throws<ArgumentException> and Assert.ThrowsAny<ArgumentException> do? Throws fails, it wants the exact type; ThrowsAny passes Assert.Throws() Failure: Exception type was not an exact match; ThrowsAny passes Confirmed entry, PitfallTests.cs, GuitarTests.cs
Does xUnit create a new instance of the test class for each test? Yes: a field incremented by each of two tests is 1 in both Both tests pass Confirmed entry, InstanceTests.cs
Run unchanged against GA’s services at 5c3a52a, what do GA’s MusicalKnowledgeServiceTests report? 0 failed, 3 skipped, 5 passed, the specialized tunings test included, because the one default tuning makes its list non-empty. Written after reading the test file, before running it 5 passed, 3 skipped, 0 failed; 1 specialized tuning, “Overall Valid: True” Confirmed entry, knowledge-tests, knowledge-results.txt
Does GA’s [SetUpFixture] TestEnvironment run before the tests of GA.Business.Core.Tests.Configuration? No: it is alone in its namespace. A probe test next to GA’s sees GA_TEST_MODE unset and its bin folder as the current directory. Written after reading the bootstrap, before running GA_TEST_MODE: (not set), Current directory is the test's bin folder: True Confirmed entry, knowledge-tests, knowledge-results.txt
What does dotnet new sln create with SDK 10.0.112? A .slnx file, not a .sln Fretboard.slnx: <Solution> and </Solution>, no project Confirmed entry, l12-project
In the folder of a solution with a library, a console app and a test project, what do dotnet build and dotnet test build and run? dotnet build builds the three projects; dotnet test runs only the tests of the test project dotnet build: Fretboard, Fretboard.App, Fretboard.Tests. dotnet test: one test assembly, 6 passed. Not predicted: after dotnet clean, dotnet test leaves Fretboard.App unbuilt Confirmed entry, l12-project
With a compile error in the console app, what do dotnet test and dotnet build do in the solution’s folder? Written after seeing the app left unbuilt, before this run dotnet test passes and exits with 0; dotnet build fails dotnet test: passed, exit code 0. dotnet build: build failed, exit code 1 Confirmed entry
What does dotnet add package CsvHelper write, and does CsvHelper read a quoted value with a comma as one field? The latest stable version; yes, where Split(',') gives one field too many Version="33.1.0" on 2026-10-03; Split: 4 fields, CsvHelper: 3 Confirmed entry, l12_csvhelper.cs
Does #:package work in a file-based app, with and without a version? #:package CsvHelper@33.1.0 restores and runs; without @version the restore refuses it 3 fields; without the version, error NU1015: The following PackageReference item(s) do not have a version specified: CsvHelper, exit code 1 Confirmed entry, l12_package_no_version.cs
Can CsvHelper’s GetRecords<Project>() fill a positional record that also has a computed property? Yes, through its constructor, by header name, ignoring SourceFiles 12 records and 632 source files; a list of them equals a list built by hand Confirmed entry, ProjectFileTests.cs
What does dotnet pack with no option make of a library that references CsvHelper? A Release build, bin/Release/Fretboard.1.0.0.nupkg, a zip with lib/net10.0/Fretboard.dll and a .nuspec that lists CsvHelper “>= 33.1.0” As predicted; the .nuspec writes version="33.1.0", NuGet’s notation for 33.1.0 or later, with exclude="Build,Analyzers". Not predicted: the readme message, the commit of HEAD in the .nuspec, and a second dotnet pack with nothing changed that prints neither line Confirmed entry, l12_ex_package.cs
Does a package with a known advisory warn, and does dotnet list package --vulnerable fail? Newtonsoft.Json 12.0.1 gives NU1903 at restore; the command lists it and exits with 0 NU1903 with GHSA-5crp-9r3c-p9vr, printed twice by a file-based app; dotnet list package --vulnerable and dotnet package list --vulnerable print the table and exit with 0 Confirmed entry, l12_audit.cs
How long do two Task.Delay(500) take, awaited one after the other and with Task.WhenAll? At least 1000 ms; under 900 ms Five runs: 1014 to 1023 ms, then 508 to 514 ms Confirmed entry, l12_async.cs
What happens to an async call without await in a top-level program? Warning CS4014, and the line written after its delay never appears CS4014 at (2,1); Before, After, exit code 0 Confirmed entry, l12_forgotten_await.cs
What does the compiler say about await in a constructor, and about an async constructor? CS4033; CS0106, modifier not valid CS4033, as predicted. public async VoicingIndex() gives CS0246 for a type async and CS0542, a member named like its type: the parser reads a method Partly refuted entry, l12_constructor_await.cs
Does GA’s AllProjects.slnx list every project file of the repository? No: at least 5 .csproj or .fsproj are missing 36 of the 111 are missing; none is listed without existing Confirmed entry, lesson12_counts.py, lesson12-results.txt
Do GA’s test projects outside the solution run in CI, and are projects outside it built anyway? Written after the previous count At least one test project is named by no workflow; at least one project outside is referenced by a listed one Six test projects, 102 lines with a test attribute, named by no workflow; 7 projects outside referenced, GA.Business.Core by 4 Confirmed entry, lesson12_counts.py, lesson12-results.txt
Does GA.Business.Core.Graphiti.Tests compile? Written after reading its project file and test No: it uses GA.Business.Graphiti without a ProjectReference, so CS0246 5 errors: CS0234 on lines 7 and 8, then CS0246 Confirmed entry, ga-probes
Is a package declared with several versions across GA’s project files? Yes, at least one 35 packages; Microsoft.Extensions.Hosting and MongoDB.Driver with five versions each Confirmed entry, lesson12_counts.py, lesson12-results.txt
Do the PackageReference Update items of GA’s Directory.Build.props change a version? Written after reading the file No: Microsoft.Extensions.Http stays at the 9.0.10 of the Graphiti tests’ project file 9.0.10; with the same line in Directory.Build.targets, 10.0.0, while Microsoft.Extensions.Options, updated in the props file only, stays at 9.0.10 Confirmed entry, ga-probes
Does GA’s non-test code block on tasks in at least 20 places? Yes 27 matches of .Result, .Wait() and GetAwaiter().GetResult(): 4 are SemaphoreSlim.Wait(), 7 read tasks after await Task.WhenAll, 5 are in files removed from compilation; 11 remain, 3 of them in constructors Refuted entry, lesson12_counts.py, lesson12-results.txt
Does GA have an async void method that isn’t an event handler? Yes, at least one None at all Refuted entry, lesson12_counts.py
Does GaCLI run from another folder without appsettings.yaml, and does sync-mongodb start? Written after reading Program.cs It prints its help; sync-mongodb stops on a missing MusicalObjectsService, from either folder Help printed; Error during sync: Unable to resolve service for type 'GA.Data.MongoDB.Services.MusicalObjectsService'... from both folders, exit code 0 Confirmed entry, ga-probes

2026-09-14 — The SDK and file-based apps

Section titled “2026-09-14 — The SDK and file-based apps”
  • My machine has two SDKs: 10.0.112 and 11.0.100-preview.3.26207.106. Without a global.json, dotnet picks the preview. The course folder pins 10.0.100 with rollForward: latestFeature, which selects 10.0.112 here.
  • WinGet offers Microsoft.DotNet.SDK.10 in version 10.0.401 on the same day: a newer feature band than mine. The file-based apps page marks #:include as available from SDK 10.0.300, so lesson 1 says that a file-based app is one file by default, and that #:include adds others from that SDK on. I haven’t tried #:include: my SDK is older.
  • The examples are file-based apps rather than one project per lesson: a beginner types one file and runs it, with nothing to set up. A single dotnet run app.cs took 0.9 s the first time and 0.2 s the next.
  • Other .cs files in the same folder are not compiled with the app: hello.cs ran fine next to a file full of errors.
  • Warnings are printed only when the SDK compiles. A second dotnet run of an unchanged file prints no warning, and dotnet run --no-cache doesn’t help either: it skips the up-to-date check of the file-based app, but MSBuild still finds the compiled output up to date and skips the compiler. dotnet clean app.cs before each run brings the warnings back; check.sh does that, and lesson 3 tells the reader.
  • Compiler errors go to standard output, with the full path of the file; The build failed. Fix the build errors and run again. goes to standard error. check.sh merges both and keeps only the file name, so that the expected files are the same on every machine.
  • Syntax errors hide the others: in the broken program of lesson 1’s exercise 2, the misspelled Writeline (CS0117) is only reported once the missing ; and quote are fixed. The exercise is built on that.
  • Standard input: when the input comes from a file, the typed text doesn’t appear in the output, so the expected files contain the prompts followed directly by the answers. The lessons show the terminal as a person sees it, with the typed lines, and say so.
  • Culture: my Windows culture is en-CA. With es-ES, double.TryParse("1.5") returns true and 15, since the dot is the thousands separator in Spanish; with fr-FR, it returns false. The example sets each culture explicitly, so the output is the same on every OS. The other examples only use formats that print the same in en-CA, en-US and the invariant culture of the Linux runner.
  • Commit b19a296, run 34914488934: green on the three OSes. actions/setup-dotnet with the course’s global.json installed SDK 10.0.401 on all three runners, while the outputs were captured with 10.0.112: every compiler message is identical. Jobs took 54 s on Linux, 1 min 36 s on macOS and 2 min 18 s on Windows, with a dotnet clean and a build for each of the 58 file-based apps.
  • Math.Pow(2, 7 / 12.0) printed with all its digits, 164.81377845643496, is the same on the three OSes.
  • The shebang line #!/usr/bin/env -S dotnet -- works on the Linux and macOS runners after chmod +x, and dotnet run ignores it on Windows.
  • Exit codes of an unhandled exception differ, so check.sh prints them and compares only exit crash:
    • Linux and macOS: 134 (the process aborts, SIGABRT) for all three crashing examples;
    • Windows, seen from Git Bash: 127 for IndexOutOfRangeException and SwitchExpressionException, and 139 with a “Segmentation fault” message for NullReferenceException;
    • Windows, seen from PowerShell: 0xE0434352 (-532462766), the code of a .NET exception, for IndexOutOfRangeException, but 0xC0000005 (-1073741819), an access violation, for NullReferenceException.
  • The examples use Guitar Alchemist as small data: the standard tuning of Tuning.Default at commit a826864, and twelve project names of code/ladybugdb/data/ga/projects.csv, extracted at commit a26a7893. Nothing in these lessons revealed a problem in GA.
  • Lesson 5 now teaches construction, instance identity, private fields, public properties, methods and a shared static count, then asks the reader to build a PracticeSession class. The English, French and Spanish pages share the same tested C# snippets and compiler output.
  • On Windows, SDK 10.0.112, C:/Program Files/Git/bin/bash.exe check.sh passed the course’s examples, exercise solutions and rejected snippets, including the new l05_* files. The two new rejected snippets produced CS0122 and CS0200, respectively. The host’s default bash was WSL and could not find the Windows dotnet; that first run failed because of the test environment, not the source.
  • This local result is not a new three-OS CI run and is not proof that the lesson is publicly deployed. Lessons 6–12 remain only in the outline.

2026-09-30 — Lesson 5 in CI on three OSes

Section titled “2026-09-30 — Lesson 5 in CI on three OSes”
  • Run 36664604662 of the C# for beginners examples workflow, on commit 669d42a of PR #60, passed on ubuntu-latest, windows-latest and macos-latest. In each job’s output, check.sh prints ok for l05_objects, l05_ex_practice, l05_get_only_property and l05_private_field: the outputs and the CS0122 and CS0200 diagnostics match expected/ on the three OSes.
  • Lesson 6 contrasts a struct with a class (copy on assignment, copy as an argument, Equals), then records (== on the data, ToString, with, readonly record struct) and enums (numbered from 0, default value, casts, Enum.IsDefined, Enum.Parse). It has four examples, three exercises and five rejected snippets, whose diagnostics are CS0019, CS1612, CS8852 (three times in two files) and CS0266. A script checked that the three locales share the same code blocks, comments aside, and that each block matches its file in code/csharp-beginner and expected/.
  • On Windows, SDK 10.0.112, under Git Bash, check.sh exited 0 with 75 ok lines, the 12 l06_* files included. This is not yet a three-OS CI run.
  • Then in CI: run 36865237779 of the C# for beginners examples workflow, on commit 114b5ed of PR #93, passed on ubuntu-latest, windows-latest and macos-latest. Each job prints ok for the 12 l06_* files; the jobs count 76 ok lines on Linux and macOS and 75 on Windows, because check.sh runs the shebang example with ./ only on Linux and macOS.
  • Surprises, each kept in the lesson: CS8524 warns about a switch expression that has an arm for every name, and its example is (ChordQuality)5 while the program fails on 7; the literal 0 converts to an enum without a cast, while 2 gives CS0266; a List<T> of mutable structs rejects shape[0].Fret = 5 with CS1612.
  • A draft of exercise 3 said that shape[i].Fret += 2 on a list of readonly record struct gives the same CS1612. A probe compiled before publishing gave CS8852 instead; the text was corrected, and compile_fail/l06_readonly_position.cs now keeps both CS8852 lines. Two other sentences rest on probes that stayed out of the course code: storing a modified copy back with shape[0] = position; printed 5, and List<T>.Contains on a class without its own equality answered False.
  • Dogfooding: at GA commit 5c3a52a, ChordQuality starts with Other, and PositionLocation, Str and Fret are readonly record struct types. Nothing in this lesson revealed a problem in GA.
  • Lesson 7 builds Guitar and Ukulele on a StringInstrument base class (base(...), virtual, override, base.Describe(), ToString), then an abstract Instrument with an abstract Play, then an IHasRange interface that a class and a record both implement. It has four examples, one of which compiles with warning CS0114, three exercises and five rejected snippets, whose diagnostics are CS0506, CS0509, CS0144, CS0534 and CS0535. The script used for lesson 6 checked the 20 code blocks of the three locales against code/csharp-beginner and expected/.
  • On Windows, SDK 10.0.112, under Git Bash, check.sh exited 0 with 87 ok lines, the 12 l07_* files included. This is not yet a three-OS CI run.
  • Then in CI: run 36893689911 of the C# for beginners examples workflow, on commit b347f7d of PR #95, passed on ubuntu-latest, windows-latest and macos-latest. Each job prints ok for the 12 l07_* files; the jobs count 88 ok lines on Linux and macOS and 87 on Windows, for the same reason as in lesson 6.
  • Two sentences of the draft had no run behind them and changed before publishing. That only a variable of type Ukulele finds a hiding method is now a line of examples/l07_hiding_warning.cs, which prints a ukulele. “The MIDI numbers are those of lesson 4, where C4 is 60” was wrong, since lesson 4 gives no MIDI number; the sentence now says that MIDI numbers count semitones, 12 per octave.
  • Dogfooding: at GA commit 5c3a52a, ChordTemplate is an abstract record whose derived records TonalModal and Analytical override Name, and whose own ToString() => Name is not sealed. The records page says that a sealed ToString stops the compiler from synthesizing one in derived records (built-in formatting for display), so this one should be shadowed. A probe confirmed it: a console project referencing GA.Domain.Core and GA.Domain.Services at 5c3a52a, built with SDK 10.0.112, printed TonalModal { Name = Major 7th, PitchClassSet = 0 4 7 E, … } for ToString() and for $"{template}" on a TonalModal, on an Analytical and on ChordTemplateFactory.CreateModalChords(...).First(), and reflection named the derived record as the type that declares ToString. A replica of the same shape with public sealed override string ToString() => Name printed maj7. One call site depends on it: ContextualChordsController.cs:301 passes template.ToString() to GetVoicingsForChordAsync, which logs it (FretboardServices.cs:536); the HTTP response takes the chord name from the route, not from this string. The finding is in the QA table; it has not been reported to GA.
  • Lesson 8 catches exceptions with try and catch, with a diagram of the exception classes; runs finally after a return, after a catch and before an exception leaves the method; throws an ArgumentOutOfRangeException with nameof; then returns to null: CS8600 and CS8618, required, the ! operator, and WarningsAsErrors=nullable set with #:property. It has six examples, four exercise files (one of them the starting point of exercise 3, with four warnings) and five rejected snippets, whose diagnostics are CS0160, CS0165, CS0029, CS9035 and CS8602 as an error. The script used for lessons 6 and 7 checked the 28 code blocks of the three locales against code/csharp-beginner and expected/.
  • On Windows, SDK 10.0.112, under Git Bash, check.sh exited 0 with 102 ok lines, the 15 l08_* files included. This is not yet a three-OS CI run.
  • The first CI run of PR #109 failed on Linux only (run 36968274101): the Linux runner printed the warnings of l08_null_warnings.cs and l08_ex_null_start.cs in line order, while Windows, locally and in CI, and macOS printed CS8618 first. The messages themselves were identical. check.sh now puts the compiler messages first, errors before warnings, each group sorted by line and column, and the lesson shows that order. No expected file of lessons 1 to 7 changed: their messages already came in that order.
  • Then, with that fix: run 37009963497 of the C# for beginners examples workflow, on commit e2a95ef of PR #109, passed on ubuntu-latest, windows-latest and macos-latest. Each job prints ok for the 15 l08_* files; the jobs count 103 ok lines on Linux and macOS and 102 on Windows, for the same reason as in lesson 6.
  • Surprises, each kept in the lesson: throw "fret out of range"; gives CS0029, a conversion error, where I expected CS0155 (The type caught or thrown must be derived from System.Exception); string capo = null; on a local variable gives CS8600, not CS8625, while GA’s test file below got CS8625 for a null argument; in l08_null_warnings.cs, CS8618 sits on the property’s declaration and the line that crashes has no warning; tuner off is printed before the value that Tune returns.
  • One sentence rests on a probe that stayed out of the course code: without its check, NoteOfString(standard, 7) fails with an IndexOutOfRangeException.
  • Dogfooding at GA commit 5c3a52a, with the hypotheses written before measuring in the Experiments table, and the probes and their instructions in code/csharp-beginner/ga-probes:
    • NoWarn lists fifteen nullable warnings in Directory.Build.props:21, and again in Directory.Build.targets:7. I expected more than 100 hidden warnings in GA.Domain.Core; with both lists removed, GA.Domain.Core and GA.Core have none. ZzNullableCanary.cs, seven nullable mistakes added to GA.Domain.Core, compiled without a warning with the lists, and with its seven warnings (CS8600 ×2, CS8602 ×3, CS8618, CS8625) without them. The hypothesis is refuted, and the QA table keeps the finding: the lists only hide the mistakes still to come.
    • new Fret(50), Fret.FromValue(50) and Fret f = 50; all throw an ArgumentOutOfRangeException, for 50 and for -2, as expected, and Fret.TryCreate(50) returns Failure(Fret number must be between -1 (muted) and 36, got 50). The exception’s message names a private constant, GA.Domain.Core.Instruments.Primitives.Fret value (50) cannot be greater than _maxValue (36). (Parameter 'value'), because IRangeValueObject.cs builds it from the caller’s expressions, and its ActualValue stays empty.
    • The open question of 2026-10-01 has its answer: new PositionLocation(5, 3), with the string and the fret swapped as plain numbers, compiles without a warning and builds string 5, fret 3. A build with every analyzer on (AnalysisLevel=latest-all) only asked for explicit types instead of var on those lines. The implicit conversions from int (Str.cs:45, Fret.cs:72) let the swap through.
  • Lesson 9 covers Dictionary<TKey, TValue>: the indexer, Add, TryGetValue, counting with GetValueOrDefault, and the order of a foreach. It covers HashSet<T>, with its set operations on copies and SetEquals. It introduces lambdas, then LINQ on lesson 4’s GA projects, now records: Count, Where, Select, OrderBy, ThenBy, First, FirstOrDefault, Any, All, Take and ToList. It ends with deferred execution, and with changing a collection inside its own foreach. It has ten examples, four exercises and four rejected snippets: CS1503, CS0021, CS0266, and CS0029 with CS1662 on the same column. The block checker matched the 37 code blocks of each locale with code/csharp-beginner and expected/.
  • Lesson 4 said that lesson 5 would replace its two arrays of projects with one list; lesson 5 didn’t. Lesson 9 does, and lesson 4 now points to it.
  • On Windows, SDK 10.0.112, under Git Bash, check.sh exited 0 with 120 ok lines, the 18 l09_* files included. This is not yet a three-OS CI run.
  • Then in CI: run 37061776660 of the C# for beginners examples workflow, on commit 690201d of PR #113, passed on ubuntu-latest, windows-latest and macos-latest. Each job prints ok for the 18 l09_* files; the jobs count 121 ok lines on Linux and macOS and 120 on Windows, for the same reason as in lesson 6.
  • Measured before writing:
    • C# 14 has no dictionary expression. Dictionary<string, int> d = ["C": 0, "E": 4]; gives CS1003 and eight other syntax errors, so the lesson uses an index initializer, { ["C"] = 0 }.
    • First with no match throws Sequence contains no matching element. This probe stayed out of the course code.
    • The two dictionary experiments of the table. The documentation backs both: “The order in which the items are returned is undefined”, and Remove “may be safely called without invalidating active enumerators” since .NET Core 3.0.
  • Dogfooding at GA commit 5c3a52a, with the hypothesis written before measuring in the Experiments table:
    • A survey of the collections in GA’s code, read at that commit, gave the lesson’s examples:
      • ChordVocabulary.PitchClasses (ChordVocabulary.cs:23), a case-insensitive dictionary read with TryGetValue, where "C#" and "Db" both give 1. Its remarks say that two earlier copies “had drifted”; git grep for ["Db"] = 1 or { "Db", 1 } finds 18 files with a table of their own, this one included.
      • PitchClassSet, an IReadOnlySet<PitchClass> over an ImmutableSortedSet (PitchClassSet.cs:37), equal when its Id, computed from the notes, is.
    • MusicalKnowledgeService.GetArtistBreakdown keeps GetAllArtists().Take(20), the first 20 by name, then sorts them by count into a Dictionary. I expected more than 20 artists, and some left out with more entries than one kept. artists.cs, built against GA.Domain.Services extracted with git archive into the ignored .ga folder, found 16, so none is left out. Two runs gave the same output.
    • The reason is the first QA row of the day: three of the four YAML files don’t deserialize, and each loader catches the exception and keeps a one-item default. The probe prints the loaders’ messages. A deserialization test outside the course code located the three lines, and I read each one against the class it fills. The three lines are unchanged at GA’s main ae196bf, the latest the local clone knows.
    • MusicalKnowledgeServiceTests prints ArtistBreakdown.Take(5) under === Top Artists === (MusicalKnowledgeServiceTests.cs:51), which relies on the dictionary’s order too.

2026-10-03 — A fretboard diagram drawn by C#

Section titled “2026-10-03 — A fretboard diagram drawn by C#”
  • Lesson 3 now shows the notes of frets 0 to 5 as a diagram, below the output of l03_fretboard.cs. The question was whether ComfyUI or Blender should draw some of the course’s pictures. A generated image can get the number of strings or frets wrong, and this diagram must show exactly the notes the program prints, so a C# program draws it: tools/fretboard.cs, with the same open strings and note names as the lesson.
  • The SVG has no words, only note names and numbers, so the three locales share one file, src/assets/csharp-beginner/l03-fretboard.svg. The lesson imports it as a component, like the music theory course, so it follows the light or dark theme. Lesson 3 became an .mdx file for that; its URL doesn’t change. The description for screen readers is translated.
  • check.sh runs tools/fretboard.cs -- --check, which fails if the committed SVG differs from what the program draws. Before trusting it, I changed one note of the file by hand: the check printed is not up to date and exited 1. A copy with Windows line endings passes. On Windows, check.sh now prints 121 ok lines.
  • The tool finds the repository from its own folder, AppContext.GetData("EntryPointFileDirectoryPath"), not from the current directory, and writes every coordinate as an int: under the fr-FR culture, $"{2.5}" gives 2,5. Both points matter for files, the subject of lesson 10.
  • Rendered with headless Edge in a light page, a dark page and at 343 pixels wide, a phone’s width: the 36 notes match the program’s output line by line. That day, headless Chrome and the DevTools screenshot both timed out on this machine.
  • Then in CI: run 37142282699 of the C# for beginners examples workflow, on commit 9229f15 of PR #126, passed on ubuntu-latest, windows-latest and macos-latest. Each job prints ok fretboard svg; the jobs count 122 ok lines on Linux and macOS and 121 on Windows, for the same reason as in lesson 6.
  • Lesson 10 is written in three locales: File, Path, the current directory, line breaks, a CSV file, numbers and cultures, streams and using, encodings, and what Guitar Alchemist does with its files. It has 11 examples, 4 exercises and 3 rejected snippets, each compared with expected/ by check.sh. The lesson is an .mdx file, for the tabs that show a path on each OS.
  • data/ga-projects.csv is new: lesson 4’s twelve projects with their numbers of .cs and .fs files at GA’s 5c3a52a, counted with git ls-tree in each project’s folder. It has two surprises. GA.Business.Config, an F# project, holds 8 C# files, the services that lesson 9 met. GA.Presentation has no source file, only its .csproj. .gitattributes gives *.csv LF line endings, so the file is the same on every OS.
  • Eight hypotheses were written before measuring, and all eight are confirmed (Experiments table). Two weren’t blind: writing tools/fretboard.cs earlier that day had shown EntryPointFileDirectoryPath, and $"{2.5}" under fr-FR. E14’s hypothesis came from a subagent’s reading of GA, before I read ScaleVideoUrlById myself.
  • check.sh runs l10_where.cs a second time from the root of the repository and compares it with expected/l10_where_root.txt, so the CI checks on three OSes that the same program misses its file from another folder. The root has no global.json: there, on my machine, dotnet picks the SDK 11.0.100 preview instead of 10.0.112, another choice made by the current directory. The output is the same.
  • TextFieldParser compiled with using Microsoft.VisualBasic.FileIO; and no package: it comes with .NET.
  • The probe files.cs runs GA’s loaders three times, as its README describes: as built, after deleting the YAML files next to itself, and from the root of the extracted GA code. Its build prints IL3000 on its own use of Assembly.Location, the call that ScaleVideoUrlById’s fallback relies on: in a single-file app it returns an empty string.
  • Six new QA rows come from the probe and from reading GA’s file code. A subagent listed the candidates; I read each row’s lines again at 5c3a52a before writing it. Two of the rows are read in the code only, not run.
  • Lesson 9’s YAML loading failure is now GuitarAlchemist/ga#797, filed on 2026-10-03 with links pinned to GA’s main, ba3b9ac, where the three lines are unchanged.
  • On Windows with SDK 10.0.112, check.sh exits 0 with 140 ok lines, the 19 of lesson 10 included. This is not yet a three-OS CI run.
  • Then in CI: run 37145323198 of the C# for beginners examples workflow, on commit 8b9fc03 of PR #129, passed on ubuntu-latest, windows-latest and macos-latest. Each job prints ok for the 19 l10_* checks, the run from the repository root included; the jobs count 141 ok lines on Linux and macOS and 140 on Windows, for the same reason as in lesson 6.
  • Lesson 11 is written in three locales: a test by hand, a library and a test project, [Fact] and dotnet test, [Theory], four tests that fail on purpose, one object per test, the public surface, and Guitar Alchemist’s tests. The code is in l11-tests: the library Fretboard, with methods of lessons 4, 8 and 10 unchanged; Fretboard.Tests, 26 tests with the exercise solutions; Pitfalls.Tests, 6 tests of which 4 fail on purpose. examples/l11_by_hand.cs and compile_fail/l11_private.cs run with the other files.
  • The projects come from dotnet new classlib, dotnet new xunit and dotnet add reference, run in the course folder; the lesson quotes their messages. The template creates an xUnit v2 project, while xUnit’s site says v2 is in maintenance mode and encourages v3.
  • check.sh runs dotnet test on both test projects after dotnet clean, so that the build’s warnings are printed every time, and keeps a summary: the warnings, each failed test and its message, the last line. It drops the paths, the durations and the stack traces, and sorts the failed tests by name: xUnit reported the four failures in another order than the file. The arrows ↓ and ↑ of a collection failure came through Git Bash on Windows; the CI will show whether they do on the three OSes.
  • Seven hypotheses were written before measuring, and all seven are confirmed (Experiments table). The last two were written after reading GA’s test file and its bootstrap, before running them.
  • Observed without a hypothesis: from a test project, a library member marked internal gives CS0117, “does not contain a definition”, not CS0122, in a scratch pair of projects; the analyzer of the xunit package warns about swapped arguments (xUnit2000) before the tests run.
  • The first failure of the lesson is a check by hand: FretFrequency(246.94, 1) gives 261.62, and a table gives 261.63 for middle C, because the open B string is already rounded. The program ends with exit code 0 anyway, which motivates a framework.
  • The probe knowledge-tests is an NUnit project that compiles four files of GA.Business.Core.Tests unchanged from the extracted code: MusicalKnowledgeServiceTests.cs, TestEnvironment.cs, GlobalUsings.cs and AssemblyInfo.cs, with the package versions of that project. ProbeTests.cs is its only own file. Its build prints only IDE0028 in GA’s ScaleMetadataRegistry.cs. Its run: 9 tests, 6 passed (GA’s 5 and the probe’s), 3 skipped.
  • This answers the journal’s open question: no GA test fails when a YAML file of GA.Business.Config stops loading. The question is removed from the list below.
  • On Windows with SDK 10.0.112, check.sh exits 0 with 144 ok lines, the 4 of lesson 11 included. This is not yet a three-OS CI run.
  • Then in CI: run 37170066128 of the C# for beginners examples workflow, on commit c3427a9 of PR #135, passed on ubuntu-latest, windows-latest and macos-latest. Each job prints ok for the 4 l11_* checks; the jobs count 145 ok lines on Linux and macOS and 144 on Windows, for the same reason as in lesson 6. l11_Pitfalls.Tests passes on Windows too, so the arrows ↓ and ↑ of the collection failure come out of Git Bash unchanged there.
  • Lesson 12 is written in three locales: a solution with lesson 11’s library, a console app and a test project; exit codes; a package from NuGet in a file-based app and in the library; a package with a known vulnerability; dotnet pack; a first look at async; and Guitar Alchemist’s solution, package versions, command-line tool and blocking waits. The code is in l12-project: Fretboard, lesson 11’s library with Guitar unchanged, Project without Parse, and ProjectFile, which reads the CSV file with CsvHelper 33.1.0; Fretboard.App, with three commands; Fretboard.Tests, 6 tests. Four examples, two rejected snippets and four exercise solutions are file-based apps.
  • check.sh packs the library before the loop, since exercises/l12_ex_package.cs reads the package, and deletes the old package first: with nothing changed, dotnet pack keeps it and prints neither of its two lines. It runs dotnet test on the solution after dotnet clean and checks that the console app is still unbuilt, then dotnet build, whose output it reduces to the project names, sorted, and the counts. It runs the app with six commands and prints each exit code. A new sed rule removes the folder from the NuGet messages of a #:package line, file.csproj : warning NUxxxx.
  • Twelve hypotheses about the SDK and nine about GA were written before measuring, some after reading the code they test (Experiments table): 16 confirmed, 1 partly refuted, 2 refuted. The refuted ones are about GA’s waits: 11 compiled blocking waits, not 20 or more, and no async void at all.
  • Observed without a hypothesis: dotnet test on a solution doesn’t build the console app, which led to the hypothesis on a compile error; the NuGet audit warning comes twice in a file-based app; the .nuspec records the commit of HEAD; 12 of GaCLI’s 28 commands throw NotImplementedException, and GA’s Quality Check run 37167745677 is green with a step that ended with exit code 134.
  • The probes are in ga-probes: lesson12_counts.py reads the commit with git only, and lesson12-results.txt holds its output; the build of GA.Business.Core.Graphiti.Tests and its restore, with a control copy whose Update line is in Directory.Build.targets; GaCLI, built from the 18 projects of its references in 28 s with 21 warnings and run from two folders.
  • This settles the item to verify about GaCLI started from another folder: it runs, and at this commit appsettings.yaml changes nothing. The item is removed from the list below, and the QA row is updated.
  • On Windows with SDK 10.0.112, check.sh exits 0 with 158 ok lines, the 14 of lesson 12 included. This is not yet a three-OS CI run.
  • The PR’s first CI run, 37174610693, failed l12_audit and l12_package_no_version on the three OSes and passed the 12 other checks of lesson 12. CI runs SDK 10.0.401, which global.json allows (rollForward: latestFeature), and that SDK names the project behind a file-based app l12_audit.cs.csproj, where 10.0.112 says l12_audit.csproj. The sed rule expected no dot in the name and left the folder in. check.sh now writes file.csproj for both, and the lesson gives both names.
  • Then in CI: run 37175088215 of the C# for beginners examples workflow, on commit 87dbafd of PR #137, passed on ubuntu-latest, windows-latest and macos-latest, all three with SDK 10.0.401. Each job prints ok for the 14 l12_* checks; the jobs count 159 ok lines on Linux and macOS and 158 on Windows, for the same reason as in lesson 6. The NuGet audit warning of l12_audit.cs comes out on the three runners, and the sorted output of the build is the same.

2026-10-04 — Fixes proposed to Guitar Alchemist

Section titled “2026-10-04 — Fixes proposed to Guitar Alchemist”

Thirteen rows of the QA table went back to GA as five pull requests, opened from a separate clone at main 75959835. Each one measures the defect before and after the fix, and GA’s CI passes on all five. None is merged yet: GA leaves the review and the merge to someone other than the author.

PR QA rows Change Before → after
#808 The three YAML files (issue #797); the tests that can’t see a fallback The three files load; the three [Ignore] tests run, and a new test fails when a file falls back to its default item With the old files, 5 of the 9 tests fail, the new one with Expected: greater than 1 But was: 1 for each broken file
#811 ReloadConfiguration, GetArtistBreakdown, the CSV’s culture, its row count, sync-mongodb A new Lazy instead of FieldInfo.SetValue; the sort before Take(20); FormattableString.Invariant; the header left out of the count; Environment.Exit(1) FieldAccessException → the four files reload; < 6, 8, 8, … > values per row under fr-FR → 6; 3 rows counted for a header and two rows → 2; sync-mongodb exits with 0 → 1
#812 The test projects outside AllProjects.slnx, the PackageReference Update items, the security scan The items move to Directory.Build.targets; three test projects join the solution; the scan fails on an advisory that a baseline doesn’t list NU1605 downgrades hidden by NoWarn: 80 → 40; 56 more tests in CI; 21 advisories in the baseline
#816 A chord template’s ToString, the [SetUpFixture] ChordTemplate.ToString is sealed; TestEnvironment moves to the project’s root namespace A dump of 442 or 475 characters → the name; GA_TEST_MODE unset → 1, with the repository root as current directory
#817 ScaleVideoUrl GA.Domain.Core.csproj embeds scale_video_urls.json, and ScaleVideoUrlById reads it under its default name Sets with a video URL: 0 → 1,490 of 4,096
  • With #808’s YAML, GA has 63 artists instead of 16, so GetArtistBreakdown’s cut before the sort bites: #811’s test of the breakdown fails on that data with the old code.
  • Moving the Update items alone failed restore, error NU1107: Version conflict detected for Microsoft.Extensions.Logging.Abstractions in GA.Infrastructure, because only five Microsoft.Extensions packages were aligned. #812 puts the 19 that ship with .NET and ASP.NET Core at one version, 10.0.6, the highest one a project or a package asks for.
  • Three of the six test projects outside the solution, which no workflow names, join it: the Graphiti tests, with the project reference they lacked, ProbabilisticGrammar’s and TabConversion.Api’s. GaChatbot.Tests and Tests/GaApi.Tests don’t compile on main either, and FloorManager.Tests.Playwright needs a running FloorManager.
  • GA’s risk-report check fails on #812 by design: its policy forbids an agent to change .github/workflows and counts Directory.Build.*, *.csproj and *.slnx as one-way doors, so a maintainer has to review it.
  • #817 changes a .csproj, which GA’s risk policy counts as a one-way door, so it is a pull request of its own.
  • #816’s first CI run failed on a test of another project that gives PowerShell 10 seconds; the same test had failed on another branch on 2026-05-23, and the rerun passed.

2026-10-06 — GA PRs #816 and #817, rechecked

Section titled “2026-10-06 — GA PRs #816 and #817, rechecked”

GA’s #816 and #817 are still open, with no review and no comment. Their CI passes, 16 checks with 5 skipped, and GitHub reports both as mergeable without conflict, although GA’s main has moved 14 commits past their base, to 40d33747. Read through the GitHub API, the three defects are unchanged at that commit:

  • ChordTemplate’s ToString() => Name is still not sealed (ChordTemplate.cs:84).
  • ScaleVideoUrlById still reads the resource GA.Domain.Core.Scales.Data.scale_video_urls.json and the fallback path Scales/Data/ (ScaleVideoUrlById.cs:30, :38), and GA.Domain.Core.csproj still doesn’t name the file.
  • TestEnvironment is still alone in GA.Business.Core.Tests.TestBootstrap (TestEnvironment.cs:1).

Lesson 7 now shows the ToString trap next to ChordTemplate, with examples/l07_record_tostring.cs. A derived record prints Seventh { Name = Major 7th, Quality = Major 7th }; the same record under a base whose ToString is sealed prints Major 7th. Both outputs come from SDK 10.0.112, compared with expected/ the way check.sh does. The existing l07_inheritance.cs, run the same way, still matches its expected output. The GA probes weren’t rerun: they need a GA checkout outside the course’s code. Neither was check.sh as a whole.

  • How many artists GA would count once its three YAML files load. A PyYAML script over the raw files, outside the course code, estimates 63. By the same estimate, Take(20) would leave out 8 artists that have more entries than the smallest count kept, The Beatles (3 entries) among them.
  • The installation commands for Linux and macOS: only the CI’s setup-dotnet ran on those OSes.
  • The debugger walkthrough of lesson 3 in VS Code, Visual Studio and Rider, for a file-based app and for a project. VS Code 1.118 and Rider are installed on my machine; Visual Studio isn’t.
  • The license terms of the three editors, at the time of reading.
  • Do GA’s other projects, beyond GA.Domain.Core and GA.Core, have nullable warnings that the two NoWarn lists hide? Only these two were measured (2026-10-02).