diff --git a/ai-docs/HANDOFF.md b/ai-docs/HANDOFF.md index 30c4031..28608f0 100644 --- a/ai-docs/HANDOFF.md +++ b/ai-docs/HANDOFF.md @@ -6,7 +6,7 @@ Retrofit of 2.2.0 to the package-modernize standard, branch `v2-retrofit` (2026- ## In progress 2.3.0 is released and verified (release run 36419884168, verify-published 36421937100 on three OSes, attestation, README images); PR #15 (baseline 2.3.0) merged as 65a3af8; PR #16 (wiki records) merged as 0d9d561, `ci` green on master (run 36458236323). The retrofit is complete. Records done: package-modernization #10 merged (inventory row 4 done), package-modernize #10 merged (L-105, L-106, C-20260928-4). -GitHub wiki (2026-09-28): eleven pages published (wiki commit 0bf2dd6) from the sibling working copy `D:\m4bwa\Claude\Projects\Ai\labs\DotNetRandomNameGenerator.wiki`, remote `DotNetRandomNameGenerator.wiki.git`. How to update it: notes/2026-09-28-github-wiki.md. +GitHub wiki (2026-09-28): eleven pages published (wiki commit 0bf2dd6) from the sibling working copy `D:\m4bwa\Claude\Projects\Ai\labs\DotNetRandomNameGenerator.wiki`, remote `DotNetRandomNameGenerator.wiki.git`. How to update it: notes/2026-09-28-github-wiki.md. Brought under wikiwright's saved-output rule the same day (wiki commit f0bb65b): notes/2026-09-28-wiki-verify.cs prints every page output, its output is saved beside it, and `wikiwright.py outputs` (0.3.0) finds all 28; one Recipes sentence about the longest last name was corrected. ## Decisions made this session decisions/2026-09-28-retrofit-without-code-changes.md (accepted; the place-list part overruled: fixed now as 2.3.0). diff --git a/ai-docs/log.md b/ai-docs/log.md index dbb9cbb..8d311e5 100644 --- a/ai-docs/log.md +++ b/ai-docs/log.md @@ -90,3 +90,8 @@ Append-only. One line per operation: `## [YYYY-MM-DD] op | title` where op is on ## [2026-09-28] verify | Retrofit closed - Mark merged PR #16 (0d9d561); `ci` on master succeeded (run 36458236323). No open pull requests or issues. The nuget.org registration lists no deprecations yet on 1.0.5.1, 1.1.0, 1.1.1, 1.2.0, 1.2.1 (Mark's step, web UI only). Nothing further owed by the agent for 2.3.0. + +## [2026-09-28] update | Wiki under wikiwright's saved-output rule for 2.3.0 +- notes/2026-09-28-wiki-verify.cs written from wikiwright's NuGet template: every output on the pages (seeded values exactly, unseeded examples after a check that they are on their list, the F# and PowerShell snippets run as written with dotnet fsi and pwsh 7.6.6, and the counts the pages state). Two runs identical; output saved LF as notes/2026-09-28-wiki-verify.out.txt. +- wikiwright 0.2.0's `outputs` read none of the 28 outputs (all shown as comments); 0.3.0 reads them: 28 checked, 0 missing after the page changes. +- Wiki commit f0bb65b: Recipes' "The longest last name is `Hollingsworth`" corrected (250 last names have 13 letters, some cut off, such as `Christopherso`; two place names share the longest length); three seeded comments quoted; the `dotnet run names.cs` fence tagged `sh`. `check` 0 errors, live check clean. diff --git a/ai-docs/notes/2026-09-28-github-wiki.md b/ai-docs/notes/2026-09-28-github-wiki.md index e37ab5b..bcc829a 100644 --- a/ai-docs/notes/2026-09-28-github-wiki.md +++ b/ai-docs/notes/2026-09-28-github-wiki.md @@ -5,7 +5,7 @@ date: 2026-09-28 verified: 2026-09-28 stale_after: 2027-03-28 tags: [wiki, docs, 2.3.0, github] -summary: "the eleven wiki pages, where their git working copy is, how the wiki was published (the first page must be saved in the web UI before the wiki repository exists) and how to update it; read before touching the wiki or the README's thread-safety sentence" +summary: "the eleven wiki pages, where their git working copy is, the verification program and its saved output, how the wiki was published (the first page must be saved in the web UI before the wiki repository exists) and how to update it; read before touching the wiki or the README's thread-safety sentence" --- # GitHub wiki for 2.3.0 @@ -16,7 +16,12 @@ Mark asked for the repository wiki (https://github.com/m4bwav/DotNetRandomNameGe ## Updating the wiki later -Edit the markdown in the working copy below, run the everwrite checker (`python C:\Users\m4bwa\.claude\skills\everwrite\scripts\tells.py *.md`), commit and `git push`. Page names are the file names with hyphens (`Getting-Started.md` is the page "Getting Started"); links between pages are plain markdown (`[Recipes](Recipes)`). When a release changes counts, seeded examples or the version number, the pages to touch are Home, Getting started, API reference, Versions and upgrading, and the footer's version line. +1. In the working copy (see below), `git pull --ff-only`, then edit the markdown. Page names are the file names with hyphens (`Getting-Started.md` is the page "Getting Started"); links between pages are plain markdown (`[Recipes](Recipes)`). +2. Re-verify: copy `2026-09-28-wiki-verify.cs` (next to this note) to a folder outside the repository. Change the version in its `#:package` line and in the F# and PowerShell snippets inside it (the `#r` line and the NuGet cache path). Run `dotnet run wiki-verify.cs > out.txt`; the F# and PowerShell cases need `dotnet fsi` and `pwsh` on the PATH. Normalise the output to LF and diff it with `2026-09-28-wiki-verify.out.txt`. The seeded values and list counts are the same on every run, and the unseeded examples print only after a check that they are on their list, so every difference is a behaviour change and a page to fix. Save the new output over the old. +3. `python /scripts/wikiwright.py outputs out.txt` must report 0 missing. That needs wikiwright 0.3.0 or later: 0.2.0 could not read values shown in comments and checked none of this wiki's outputs. Then `wikiwright.py check --version ` and the everwrite checker. +4. Commit, `git push`, then `wikiwright.py live m4bwav/DotNetRandomNameGenerator `. + +When a release changes counts, seeded examples or the version number, the pages to touch are Home, Getting started, API reference, Recipes, Reproducible names, Versions and upgrading, and the footer's version line. ## Where the pages are @@ -35,6 +40,15 @@ The first `git push` answered `Repository not found`: the repository had `has_wi - Package 2.3.0: nupkg 942,766 bytes; netstandard2.0 DLL 995,840 bytes; the seven resources total 977,150 bytes (last names 695,422). - The unit tests have no test that shares one generator between threads; the golden capture's "8 threads" case is the only multi-thread evidence. +## Brought under the saved-output rule (2026-09-28) + +The wiki was written by hand before wikiwright 0.2.0 and kept no verification program. wikiwright's 0.3.0 session wrote `2026-09-28-wiki-verify.cs` from the skill's NuGet template. It prints every output the pages show: exactly for seeded calls, and for the unseeded examples on Home and Getting started each value only after checking it is on the list its method draws from. It runs the F# and PowerShell snippets as written, and prints the counts and names the pages state in prose. Its output, identical over two runs, is `2026-09-28-wiki-verify.out.txt`. + +- wikiwright 0.2.0's `outputs` saw none of this wiki's 28 outputs. They are shown as comments (`// "Marguerita"`, `// always "Alisa Streets"`, F# `// Boardman`, PowerShell `# 88799`) and as comment lines that close a code block, and 0.2.0 read only `//=>`. 0.3.0 reads them. +- Every seeded value, list count and name on the pages matched the run, with one exception. Recipes said "The longest last name is `Hollingsworth`". In fact 250 last names have 13 letters, the most any has, and some of them look cut off at that length (`Christopherso`); two place names share the longest length, 35 characters. The sentence now says so. +- Page changes (wiki commit f0bb65b): that sentence; the three `// Marguerita` comments on Reproducible names are quoted so the checker reads them; the `dotnet run names.cs` fence is tagged `sh`. Result: `outputs` 28 checked, 0 missing; `check` clean; everwrite 0 strong on the changed page. +- Not run: .NET Framework (the cross-runtime claims rest on the repository's golden tests, which replay on net48 in CI), Unity, and the ASP.NET Core and CSV recipes, which show no output. + ## Open item found: README thread-safety sentence The README says a default-constructed generator "is safe to share between threads (it uses `Random.Shared` on .NET 6+ and a well-seeded `Random` per generator on .NET Framework)". On the netstandard2.0 build `BaseNameGenerator` creates a plain `new Random(SeedSource.Next())` per generator, and `System.Random` is not thread-safe, so the .NET Framework half of that sentence overstates it. The wiki states the safe pattern (one generator per thread on .NET Framework). Fix the README sentence in the next README change; the README ships in the package, so it reaches nuget.org with the next release. diff --git a/ai-docs/notes/2026-09-28-wiki-verify.cs b/ai-docs/notes/2026-09-28-wiki-verify.cs new file mode 100644 index 0000000..665779d --- /dev/null +++ b/ai-docs/notes/2026-09-28-wiki-verify.cs @@ -0,0 +1,206 @@ +#:package RandomNameGeneratorLibrary@2.3.0 +#:property PublishAot=false +// wiki-verify for RandomNameGeneratorLibrary 2.3.0: prints every output the wiki's pages show, +// run against the PUBLISHED package from nuget.org, never the working tree. The repository keeps +// this file as ai-docs/notes/2026-09-28-wiki-verify.cs and its output as +// 2026-09-28-wiki-verify.out.txt (LF line endings) so the next release can run it and diff. +// +// Written 2026-09-28 when the wiki (written by hand earlier that day, no program kept) was +// brought under wikiwright's saved-output rule. Run it from a folder outside any project cone: +// dotnet run wiki-verify.cs > out.txt +// The PowerShell and F# cases need pwsh (PowerShell 7) and dotnet fsi on the PATH; they run the +// pages' snippets as written against the package in the NuGet cache. +// +// Seeded outputs are exact and identical on every run. Unseeded examples ("for example +// "Prophetstown"") cannot be reproduced; for those the program prints the value only after +// checking it is on the list the method draws from, so the page shows a possible answer. +// This runs on .NET 10 only; the repository's golden tests cover .NET Framework 4.8. +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using RandomNameGeneratorLibrary; + +Console.OutputEncoding = Encoding.UTF8; + +var asm = typeof(PersonNameGenerator).Assembly; +Show("installed", $"{asm.GetName().Name} {asm.GetCustomAttribute()?.InformationalVersion?.Split('+')[0]}"); + +var male = PersonNameGenerator.MaleFirstNames; +var female = PersonNameGenerator.FemaleFirstNames; +var last = PersonNameGenerator.LastNames; +var placeList = PlaceNameGenerator.PlaceNames; + +// ----- Home and Getting started: unseeded examples, each checked against its list ----- +bool IsFirst(string n) => male.Contains(n) || female.Contains(n); +bool IsFull(string n) => n.Split(' ') is [var f, var l] && IsFirst(f) && last.Contains(l); +bool IsCatalog(string n) => n.StartsWith("HD ") && int.TryParse(n[3..], out var hd) && hd >= 1 && hd <= 225300; +Possible("home: unseeded examples", ("Deneen Vanvolkinburg", IsFull), ("Prophetstown", placeList.Contains), ("Alpha Muscae", StarNameGenerator.StarNames.Contains)); +Possible("getting started: the first call, unseeded", ("Jon Rohl", IsFull), ("Hildegard", female.Contains), ("Benny", male.Contains), + ("Drouillard", last.Contains), ("Prophetstown", placeList.Contains), ("26 Vulpeculae", StarNameGenerator.StarNames.Contains), + ("Ankaa", StarNameGenerator.ProperStarNames.Contains), ("HD 107420", IsCatalog)); + +// ----- seeded examples: exact ----- +Show("getting started: seed 42, full name (also Recipes, seeded test data)", new PersonNameGenerator(42).GenerateRandomFirstAndLastName()); +Show("getting started: seed 42, place", new PlaceNameGenerator(42).GenerateRandomPlaceName()); +var r42 = new Random(42); +Show("getting started: extension methods on one Random(42)", string.Join("\n", r42.GenerateRandomFemaleFirstName(), r42.GenerateRandomPlaceName(), r42.GenerateRandomStarName())); +var a = new PersonNameGenerator(42); +var b = new PersonNameGenerator(new Random(42)); +var c = new Random(42); +Show("reproducible names: three ways to seed", string.Join("\n", a.GenerateRandomFemaleFirstName(), b.GenerateRandomFemaleFirstName(), c.GenerateRandomFemaleFirstName())); +var twoCalls = new PersonNameGenerator(42); +Show("recipes: first then last equals first-and-last (seed 42)", twoCalls.GenerateRandomFirstName() + " " + twoCalls.GenerateRandomLastName()); + +// ----- Recipes ----- +var people1 = new PersonNameGenerator(1); +var lastNames = new HashSet(); +while (lastNames.Count < 5) +{ + lastNames.Add(people1.GenerateRandomLastName()); +} + +Show("recipes: distinct names", string.Join(", ", lastNames)); + +Show("recipes: one Random for everything", Captured(() => +{ + var random = new Random(2026); + var people = new PersonNameGenerator(random); + var places = new PlaceNameGenerator(random); + for (var i = 0; i < 3; i++) + { + Console.WriteLine($"{people.GenerateRandomFirstAndLastName()} from {places.GenerateRandomPlaceName()}"); + } +})); +var space = new Random(2026); +Show("recipes: the same seed with a StarNameGenerator", $"{new PersonNameGenerator(space).GenerateRandomFirstAndLastName()} of {new StarNameGenerator(space).GenerateRandomStarName()}"); + +Show("recipes: usernames", Captured(() => +{ + var people = new PersonNameGenerator(99); + for (var i = 0; i < 3; i++) + { + var first = people.GenerateRandomFirstName(); + var surname = people.GenerateRandomLastName(); + Console.WriteLine($"{first} {surname} -> {first.ToLowerInvariant()}.{surname.ToLowerInvariant()}@example.com"); + } +})); + +var mc = PersonNameGenerator.LastNames.Where(n => n.StartsWith("Mc")).ToList(); +Show("recipes: picking from the lists (the Mc names)", $"{mc.Count:N0} names"); +Show("recipes: picking from the lists (three of them)", Captured(() => +{ + var random = new Random(3); + Console.WriteLine(string.Join(", ", Enumerable.Range(0, 3).Select(_ => mc[random.Next(mc.Count)]))); +})); + +// ----- facts the pages state in prose ----- +string Longest(IEnumerable names) => names.OrderByDescending(n => n.Length).First(); +Show("sizes: male, female, last, places, stars, proper, designated, catalogue", + $"{male.Count:N0} {female.Count:N0} {last.Count:N0} {placeList.Count:N0} {StarNameGenerator.StarNames.Count:N0} {StarNameGenerator.ProperStarNames.Count:N0} {StarNameGenerator.DesignatedStarNames.Count:N0} {StarNameGenerator.CatalogStarNameCount:N0}"); +Show("first entries: male, female, last, place, last place, proper, designated", + string.Join(" | ", male[0], female[0], last[0], placeList[0], placeList[^1], StarNameGenerator.ProperStarNames[0], StarNameGenerator.DesignatedStarNames[0])); +Show("places with a space, places with a non-ASCII letter", $"{placeList.Count(p => p.Contains(' ')):N0} {placeList.Count(p => p.Any(ch => ch > 127))}"); +Show("first names of four letters or fewer: female, male; on both lists", + $"{female.Count(n => n.Length <= 4)} {male.Count(n => n.Length <= 4)} {male.Intersect(female).Count()}"); +Show("longest last name, longest place name", Longest(last) + " | " + Longest(placeList)); +Show("names of the longest length: last names (length, count, first three), place names", + $"{Longest(last).Length} characters, {last.Count(n => n.Length == Longest(last).Length)}: {string.Join(", ", last.Where(n => n.Length == Longest(last).Length).Take(3))}\n" + + $"{Longest(placeList).Length} characters: {string.Join(" | ", placeList.Where(n => n.Length == Longest(placeList).Length))}"); +var every = male.Concat(female).Concat(last).Concat(placeList).Concat(StarNameGenerator.StarNames).ToList(); +Show("places with a comma", string.Join(" | ", placeList.Where(p => p.Contains(',')))); +Show("names with an apostrophe (places), with a double quotation mark (all lists)", $"{placeList.Count(p => p.Contains('\''))} {every.Count(n => n.Contains('"'))}"); +Show("non-ASCII examples on the lists", string.Join(" | ", new[] { "Mayagüez zona urbana", "Boötis", "Rosalíadecastro", "Delta² Gruis" } + .Select(n => n + (placeList.Contains(n) || StarNameGenerator.StarNames.Any(s => s.Contains(n)) ? " found" : " MISSING")))); +Show("person names are ASCII letters only", every.Take(male.Count + female.Count + last.Count).All(n => n.All(char.IsAsciiLetter)).ToString()); + +// ----- API reference: counts and errors ----- +Show("zero gives an empty sequence", new PersonNameGenerator(1).GenerateMultipleLastNames(0).Count().ToString()); +Show("negative count", Catch(() => new PersonNameGenerator(1).GenerateMultipleLastNames(-1))); +Show("negative count, places", Catch(() => new PlaceNameGenerator(1).GenerateMultiplePlaceNames(-1))); +Show("null Random", Catch(() => new PersonNameGenerator((Random)null!))); + +// ----- the pages' F# and PowerShell snippets, run as written ----- +var fsx = System.IO.Path.Combine(System.IO.Path.GetTempPath(), "rng-wiki-verify.fsx"); +System.IO.File.WriteAllText(fsx, """ + #r "nuget: RandomNameGeneratorLibrary, 2.3.0" + open System + open RandomNameGeneratorLibrary + + let people = PersonNameGenerator(42) + printfn "%s" (people.GenerateRandomFirstAndLastName()) // Alisa Streets + printfn "%s" (Random(42).GenerateRandomPlaceName()) // Boardman + """); +Show("getting started: F#", Run("dotnet", "fsi", "--quiet", fsx)); +var ps = Run("pwsh", "-NoProfile", "-NonInteractive", "-Command", """ + Add-Type -Path "$env:USERPROFILE/.nuget/packages/randomnamegeneratorlibrary/2.3.0/lib/netstandard2.0/RandomNameGeneratorLibrary.dll" + [RandomNameGeneratorLibrary.PersonNameGenerator]::new().GenerateRandomFirstAndLastName() + [RandomNameGeneratorLibrary.PlaceNameGenerator]::new(42).GenerateRandomPlaceName() # Boardman + [RandomNameGeneratorLibrary.PersonNameGenerator]::LastNames.Count # 88799 + """).Split('\n'); +// The first line is unseeded: print whether it is a possible answer, not the name, so the saved +// output stays the same on every run. +Show("getting started: PowerShell", string.Join("\n", ps.Select((line, i) => i == 0 ? (IsFull(line) ? "" : "NOT ON THE LISTS: " + line) : line))); + +static void Show(string label, object? value) +{ + Console.WriteLine($"## {label}"); + Console.WriteLine(value?.ToString() ?? ""); + Console.WriteLine(); +} + +// Prints each value only when its check passes, one per line, so a page's example is shown to be +// a possible answer of an unseeded call. +static void Possible(string label, params (string Value, Func OnList)[] cases) +{ + Show(label + " (each checked against the list it comes from)", + string.Join("\n", cases.Select(c => c.OnList(c.Value) ? c.Value : "NOT ON THE LIST: " + c.Value))); +} + +// What a page's code prints with Console.WriteLine, line for line. +static string Captured(Action action) +{ + var original = Console.Out; + var writer = new System.IO.StringWriter { NewLine = "\n" }; + Console.SetOut(writer); + try + { + action(); + } + finally + { + Console.SetOut(original); + } + + return writer.ToString().TrimEnd('\n'); +} + +// Runs a page's snippet in another host and returns what it printed, LF line endings. +static string Run(string file, params string[] args) +{ + var info = new System.Diagnostics.ProcessStartInfo(file) { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false }; + foreach (var arg in args) + { + info.ArgumentList.Add(arg); + } + + using var p = System.Diagnostics.Process.Start(info)!; + var stdout = p.StandardOutput.ReadToEnd(); + var stderr = p.StandardError.ReadToEnd(); + p.WaitForExit(); + var text = stdout + (stderr.Length > 0 ? "--- stderr\n" + stderr : "") + (p.ExitCode != 0 ? "--- exit " + p.ExitCode : ""); + return text.Replace("\r\n", "\n").TrimEnd(); +} + +static string Catch(Func action) +{ + try + { + return "returned: " + (action()?.ToString() ?? ""); + } + catch (Exception e) + { + return $"{e.GetType().Name}: {e.Message}"; + } +} diff --git a/ai-docs/notes/2026-09-28-wiki-verify.out.txt b/ai-docs/notes/2026-09-28-wiki-verify.out.txt new file mode 100644 index 0000000..68ed77b --- /dev/null +++ b/ai-docs/notes/2026-09-28-wiki-verify.out.txt @@ -0,0 +1,113 @@ +## installed +RandomNameGeneratorLibrary 2.3.0 + +## home: unseeded examples (each checked against the list it comes from) +Deneen Vanvolkinburg +Prophetstown +Alpha Muscae + +## getting started: the first call, unseeded (each checked against the list it comes from) +Jon Rohl +Hildegard +Benny +Drouillard +Prophetstown +26 Vulpeculae +Ankaa +HD 107420 + +## getting started: seed 42, full name (also Recipes, seeded test data) +Alisa Streets + +## getting started: seed 42, place +Boardman + +## getting started: extension methods on one Random(42) +Marguerita +Sedco Hills +Ran + +## reproducible names: three ways to seed +Marguerita +Marguerita +Marguerita + +## recipes: first then last equals first-and-last (seed 42) +Alisa Streets + +## recipes: distinct names +Mccaskey, Gerena, Antinoro, Rosazza, Bocklage + +## recipes: one Random for everything +Kerry Marrello from La Plena comunidad +Iris Velazques from Vanleer +Bambi Neblock from Highland Beach + +## recipes: the same seed with a StarNameGenerator +Kerry Marrello of Zeta Trianguli Australis + +## recipes: usernames +Emmitt Diak -> emmitt.diak@example.com +Nguyet Kistler -> nguyet.kistler@example.com +Joel Pieroni -> joel.pieroni@example.com + +## recipes: picking from the lists (the Mc names) +1,394 names + +## recipes: picking from the lists (three of them) +Mcnees, Mcrorie, Mccraight + +## sizes: male, female, last, places, stars, proper, designated, catalogue +1,219 4,275 88,799 16,969 3,716 640 3,076 343,518 + +## first entries: male, female, last, place, last place, proper, designated +James | Mary | Smith | Abbeville | Yaurel comunidad | Absolutno | 1 Aquarii + +## places with a space, places with a non-ASCII letter +6,394 52 + +## first names of four letters or fewer: female, male; on both lists +652 251 331 + +## longest last name, longest place name +Hollingsworth | Chesapeake Ranch Estates-Drum Point + +## names of the longest length: last names (length, count, first three), place names +13 characters, 250: Hollingsworth, Killingsworth, Christopherso +35 characters: Chesapeake Ranch Estates-Drum Point | Leisure Village West-Pine Lake Park + +## places with a comma +Islamorada, Village of Islands | Lynchburg, Moore County + +## names with an apostrophe (places), with a double quotation mark (all lists) +33 0 + +## non-ASCII examples on the lists +Mayagüez zona urbana found | Boötis found | Rosalíadecastro found | Delta² Gruis found + +## person names are ASCII letters only +True + +## zero gives an empty sequence +0 + +## negative count +ArgumentOutOfRangeException: The number of names to generate cannot be negative. (Parameter 'count') +Actual value was -1. + +## negative count, places +ArgumentOutOfRangeException: The number of names to generate cannot be negative. (Parameter 'numberOfNames') +Actual value was -1. + +## null Random +ArgumentNullException: Value cannot be null. (Parameter 'randGen') + +## getting started: F# +Alisa Streets +Boardman + +## getting started: PowerShell + +Boardman +88799 +