Spec-Driven

Spec-Driven C#: Demonstration

the built executable run against this project's own tree, 2026-09-16

Synopsis:
This page shows the C# executable run against this project's own tree, in the 11 invocations the page structure fixes.
  • Every block below is captured output. Nothing is paraphrased, reconstructed from the specification, or edited after capture.
  • Seven invocations search md, ixx, cpp, rs, cs - the longest list of the three built so far, C# being third in the cumulative order.
This page inherited two divergences from the page structure rather than introducing them, and both closed in the document's favour.
  • The fixed set went from six invocations to eleven, and [ext] became cumulative.
  • No driver changed when they closed - the document moved to where the code already was.
Being widest makes this the record with the least left over to compare, and Section 4 says how much.
  • This capture's path list contains both siblings' rather than diverging from them, but the extra sources interleave with documents every run reports - so the lists agree as sets and not as sequences.
  • One invocation of the seven escapes that: block 3.6 reads the project root where no implementation's sources sit, and all three report the same eight paths and the same two counts.
  • A reader checking the consistency guarantee against captured output has that block and no other.

1.  What the Demonstration Runs

demo/Demo.cs runs the built executable against this project's own tree, rooted at Spec_driven_TextFinder/, and shows the 11 invocations Page_Structure.md §7.1 fixes. Seven of them supply -P . -p "md, ixx, cpp, rs, cs", so each search covers the project's documents together with all three implementations' sources; the other four are this implementation's own, two rooted in its component directories, one in its structure document, and one asking for help. Every block below is captured output. Nothing on this page is paraphrased, reconstructed from the specification, or edited after capture, per Page_Structure.md §7.3. The capture is dated, and Section 5 says what that means for a reader comparing it against a fresh run. Each block takes the same six parts, per Page_Structure.md §7.2: the purpose in one or two lines, the command line echoed as $ <executable> <arguments>, at most 14 output lines followed by a ... N more line where N is the number withheld, the run summary of Spec_TextFinder.md §3.6 when that excerpt withheld it, each stderr line prefixed [stderr], and the total line count with the exit code. The count is of stdout for the whole run rather than of the excerpt. The fifth part is the newest and exists because of where §3.6 puts its line. The summary is the last thing a traversing run writes, so on every invocation emitting more than 14 lines the excerpt ends before reaching it, and a block that stopped there would withhold the one line reporting what the run reached. It is shown after the ... N more line and counted among the lines that line withholds. This page inherited two divergences from the mandate rather than introducing them, and both have since been retired in the document's favour rather than the drivers'.
  • Eleven invocations, not six. §7.1 fixed a set of six while this driver ran 11. The table now fixes eleven, and the C++ driver gained the three it lacked.
  • Five extensions, not one. §0 gave C# the value cs for [ext] while this driver searched md, ixx, cpp, rs, cs. [ext] is now cumulative - each language's own extensions appended to those of the languages before it - so this driver's list is what §0 fixes, and it is the longest of the three built so far because C# is third in the order. Python's, when it comes, will hold all five. Section 4 states what that costs.
The blocks below are still what the executable produced, since §7.3 forbids reconstructing a result from a document. No driver changed when the two divergences closed; the document moved to where the code already was, which is the reverse of this project's usual direction and worth saying plainly. Two rules govern which patterns a demonstration may use, and this set satisfies both. Every pattern stays inside the portable subset of Spec_TextFinder.md §6.1, and none of the patterns that match uses ., a character class, or a class escape, so none turns on which engine an implementation names. And every pattern has something to find in every implementation: too large is an announcement §3.4 fixes, Spec_TextFinder.md names a document every implementation searches, and interface appears in this thread's own structure document. Invocation 8's ^public is the exception that proves the rule: it is a C# keyword, so that invocation demonstrates C# and is confined to two C# directories by its own -P arguments rather than pretending to a wider search.

2.  The Header

The driver writes four fields before the first block: the date of the capture, the path of the executable, the search root, and the extension list. This is the whole of the header.
CSharp_TextFinder demonstration
  date:       2026-09-16
  executable: C:/github/JimFawcett/NewSite/Code/Projects/Spec_driven_TextFinder/CSharp_Spec_driven_TextFinder/CSharp_Spec_driven_TextFinder_Entry/bin/Debug/net8.0/CSharp_TextFinder.exe
  root:       C:/github/JimFawcett/NewSite/Code/Projects/Spec_driven_TextFinder
  extensions: "md, ixx, cpp, rs, cs"
The date comes from run_demo.bat through the TEXTFINDER_DEMO_DATE environment variable. The driver falls back to DateTime.UtcNow when the variable is absent, which reads as the previous day late in the evening, so the runner is the authority for the date a capture carries. C++ gets a local date from std::chrono::current_zone() and Rust computes a civil date from a Unix timestamp by hand; C# has DateTime.Now and does not use it, because the binary enables InvariantGlobalization and a runner that already knows the date is the simpler authority. The executable path and the root are computed from the layout CSharp_TextFinder_Structure.md fixes, by walking up from the driver's own assembly directory. Both are rendered with forward slashes on every platform, matching the rule §3.4 fixes for block lines, which is a courtesy to a reader comparing headers rather than a requirement.

3.  The Eleven Blocks

3.1  No Switch at All

The bare command line of Spec_TextFinder.md §3.1. It names no work, so CSharp_TextFinder lists the options a real invocation would start from and exits 0 without traversing. Look for the /v line: it reads false, because this listing reports an option set in which /v was never set.
1. No switch at all. The command line names no work, so CSharp_TextFinder lists
   the options a real invocation would start from and exits 0 (Spec_TextFinder.md §3.1).
  $ CSharp_TextFinder

      /P .
      /p
      /r .
      /s true
      /h true
      /v false
      /H false
      /n false
      /L false

  9 line(s), exit 0
Nine lines, matching the nine §5.3 fixes, in §5 table order. The /p line carries no value and no trailing space, which is the rule that lets this text serve as a test fixture. These nine lines are byte for byte what the other two implementations print, since §5.3 fixes the form and none of the three chooses it. This block is also the cheapest proof that Console.WriteLine is not in use: nine lines and no CR among them.

3.2  The Default Expression

The no-content case of §3.3: the default /r of . with neither /n nor /L needs no file content, so every selected file of non-zero size is reported by its path line alone and none of them is opened.
2. Default expression. The default /r of . with no /n or /L needs no file content,
   so each selected file is reported by its path line alone (Spec_TextFinder.md §3.3).
  $ CSharp_TextFinder -P . -p "md, ixx, cpp, rs, cs"

      Constitution.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Cmdline/Prompts_Fix_Spec_Cpp_TextFinder_Cmdline.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Cmdline/Prompts_Spec_Cpp_TextFinder_Cmdline.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Cmdline/Spec_Cpp_TextFinder_Cmdline.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Cmdline/src/Cpp_TextFinder_Cmdline.ixx
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Cmdline/src/Cpp_TextFinder_Cmdline_TestDriver.cpp
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Cmdline/src/Cpp_TextFinder_Cmdline_UnitTest.ixx
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/Prompts_Fix_Spec_Cpp_TextFinder_Dirnav.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/Prompts_Spec_Cpp_TextFinder_Dirnav.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/Spec_Cpp_TextFinder_Dirnav.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/src/Cpp_TextFinder_Dirnav.ixx
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/src/Cpp_TextFinder_Dirnav_TestDriver.cpp
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/src/Cpp_TextFinder_Dirnav_UnitTest.ixx
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Output/Prompts_Fix_Spec_Cpp_TextFinder_Output.md
      ... 68 more
      accessed 81 files, 36 directories

  82 line(s), exit 0
82 lines for 81 selected files, the extra one being the run summary. What the count alone does not show is that this invocation performed no reads: the size test ran on FileInfo.Length and nothing else, which is why this is among the fastest of the 11 and why a binary file among these paths would be reported rather than skipped. This is also the count that shows how far the tree has grown. The Rust capture of the day before reports 63 files over four extensions, and this one reports 79 over five: the C# sources, specifications, and project files this thread added account for the difference. Neither number says anything about either program. Nine build directories contribute nothing to that count. bin and obj are both among the 11 default skip-list names of §3.2, which is what keeps a .NET solution's output out of a search of its own source - and the reason the C# demonstration needs no special pruning where the C++ one relies on build and the Rust one on target.

3.3  The Two-Level Block

The block form of §3.4 with both optional fields on: a path written once, then an indented detail line per match carrying the line number and the line's text. The pattern too large is one of the four announcements the specification fixes, so it appears as a literal in the documents and in the sources of every implementation.
3. The two-level block of §3.4: a path written once, then an indented detail line
   per match carrying the line number and the line's text.
  $ CSharp_TextFinder -P . -p "md, ixx, cpp, rs, cs" -r "too large" -n true -L true

      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/Prompts_Fix_Spec_Cpp_TextFinder_Dirnav.md
        73 - [Spec_TextFinder.md](../../Spec_TextFinder.md) gained: symlink and non-file/non-directory root handling in §3.2; the three admission tests in §3.3 — 10 MB, no NUL byte, valid UTF-8 — with whole-file reading justified by "skipped entirely rather than searched in part"; path rendering and a four-row announcement table in §3.4 (`searched`, `skipped`, `too large`, `cannot open`) with the `/h` gating; the dotfile rule, extension case-sensitivity, the non-empty-regex requirement, and the new `/h` meaning across the `/p`, `/r`, and `/h` rows of §5; an empty-`/r` row in §5.2; and the NUL test named in §7.
        80 - - **`too large` and `cannot open` ignore `/h`; `searched` and `skipped` obey it.** Item 6 suppressed announcements but item 3 wanted failures visible; silencing a 10 MB skip under the default would hide requested work that did not happen.
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/Spec_Cpp_TextFinder_Dirnav.md
        84 - No file announcement accompanies that block. `searched` reports a file that was read and matched nothing and `skipped` reports one a content test rejected, and in this case neither happened — every selected file matches — so the library emits neither, whatever `/h` says. A selected file of zero size produces no block and draws no announcement either. The size test still runs on filesystem metadata, so a file above the limit still draws the error announcement `too large`, and one whose metadata cannot be read still draws `cannot open`.
        117 - The file count is incremented in the one place a file is examined: immediately after the `/p` test of §6 admits it and before its size is taken. Everything §3.6 excludes is therefore excluded by construction rather than by a second test — an entry refused by `/p`, a symbolic link, an entry beneath a pruned directory, an entry whose name will not render, and an entry that is neither a regular file nor a directory all fail or bypass that test and never reach the increment, though the last two draw `cannot open` on the way past. A file admitted and then announced `too large` or `cannot open` is counted, the increment standing ahead of both.
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/src/Cpp_TextFinder_Dirnav.ixx
        264 -         if (size > sizeLimit) { emit("too large " + displayPath(file)); return; }
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/src/Cpp_TextFinder_Dirnav_UnitTest.ixx
        315 -     check.equal(relative(search(root / "big.txt", commands), root), "too large big.txt",
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Output/Prompts_Fix_Spec_Cpp_TextFinder_Output.md
        33 - 1. Parent §3.4 contradicted itself in consecutive sentences — "under the default /h `true` no file announcement appears and the output holds match records alone," then "`too large` and `cannot open` are emitted whatever /h says." Both are file announcements.
        58 - > 1. treat "too large" and "cannot open" as error announcements not file announcements.  Make any name changes appropriate to this new definition
        78 - Parent §3.4 now distinguishes **file announcements** — `searched` and `skipped`, gated on `/h` — from **error announcements** — `too large` and `cannot open`, emitted whatever `/h` says — in separate tables, which retires the self-contradiction: under the default the output holds match records and error announcements. The new names propagated to the `/h` row, the §5.1 help text, Dirnav §5 rule 6 and §8, and Entry's diagnostics section. Parent §3.4 also gained the terminator rule moved out of the Output spec: a single LF for every record and announcement on every platform, with implementations obliged to stop their runtime translating it.
      ... 75 more
      accessed 81 files, 36 directories

  89 line(s), exit 0
70 lines. The two-space indent, the  -  between the number and the text, and the path appearing once above its details are all §3.4 rather than choices this implementation made. Line 255 of Cpp_TextFinder_Dirnav.ixx is the C++ implementation emitting the announcement this search is looking for, which is the demonstration reading a sibling's source. The detail lines are long because the matched lines are long. §3.4 fixes the line's text verbatim with no truncation, so a match inside a 600-character Markdown paragraph produces a 600-character detail line. The arrows at the top right of the block reach the right-hand end without scrolling to the bottom.

3.4  The Same Search with /L false

Dropping /L leaves the line number alone on each detail line. The same files, the same matches, and the same 70 lines: /n and /L select what a detail line carries and neither decides whether there is one.
4. The same search with /L false, leaving the line number alone on each detail line.
  $ CSharp_TextFinder -P . -p "md, ixx, cpp, rs, cs" -r "too large" -n true

      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/Prompts_Fix_Spec_Cpp_TextFinder_Dirnav.md
        73
        80
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/Spec_Cpp_TextFinder_Dirnav.md
        84
        117
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/src/Cpp_TextFinder_Dirnav.ixx
        264
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/src/Cpp_TextFinder_Dirnav_UnitTest.ixx
        315
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Output/Prompts_Fix_Spec_Cpp_TextFinder_Output.md
        33
        58
        78
      ... 75 more
      accessed 81 files, 36 directories

  89 line(s), exit 0
The identical count across 3.3 and 3.4 is the clearest statement of that rule the demonstration can make. A reader who suspected /L of adding lines rather than filling them has two numbers to compare.

3.5  Path-Only Blocks

Which documents cite the parent specification. With neither /n nor /L, every block is its path line, and rule 2 of the emission rules applies: the loop over a file's lines returns at the first match, so a file matching 40 times costs the same as one matching once.
5. Which documents cite the parent specification. Neither /n nor /L, so every block
   is its path line and no path is written twice.
  $ CSharp_TextFinder -P . -p "md, ixx, cpp, rs, cs" -r Spec_TextFinder\.md

      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Cmdline/Prompts_Fix_Spec_Cpp_TextFinder_Cmdline.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Cmdline/Prompts_Spec_Cpp_TextFinder_Cmdline.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Cmdline/Spec_Cpp_TextFinder_Cmdline.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Cmdline/src/Cpp_TextFinder_Cmdline.ixx
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Cmdline/src/Cpp_TextFinder_Cmdline_UnitTest.ixx
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/Prompts_Fix_Spec_Cpp_TextFinder_Dirnav.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/Prompts_Spec_Cpp_TextFinder_Dirnav.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/Spec_Cpp_TextFinder_Dirnav.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/src/Cpp_TextFinder_Dirnav.ixx
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Dirnav/src/Cpp_TextFinder_Dirnav_UnitTest.ixx
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Output/Prompts_Fix_Spec_Cpp_TextFinder_Output.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Output/Prompts_Spec_Cpp_TextFinder_Output.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Output/Spec_Cpp_TextFinder_Output.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Output/src/Cpp_TextFinder_Output.ixx
      ... 47 more
      accessed 81 files, 36 directories

  61 line(s), exit 0
61 lines for 60 files against 3.2's 82, so 21 of the selected files cite the parent specification nowhere - while the summary reports the same accessed 81 files, 36 directories as 3.2, since the same files were examined whatever the expression found in them. The escaped dot in Spec_TextFinder\.md is the only metacharacter in the whole pattern set outside invocations 8 and 10, and §6.1 puts a class escape inside the subset all four engines accept alike.

3.6  One Level Deep

The same search with -s false, which enters no subdirectory. What remains is the project root's own documents.
6. The same search one level deep, /s false entering no subdirectory.
  $ CSharp_TextFinder -P . -p "md, ixx, cpp, rs, cs" -r Spec_TextFinder\.md -s false

      Page_Structure.md
      Project_Tree.md
      Prompts_Fix_Constitution.md
      Prompts_Fix_Spec_TextFinder.md
      Prompts_Page_Structure.md
      Prompts_Spec_TextFinder.md
      README.md
      Text_Tone.md
      accessed 13 files, 1 directories

  9 line(s), exit 0
Nine lines and no ... N more, since a run emitting 14 lines or fewer shows all of them - eight paths and the summary, whose accessed 13 files, 1 directories is the clearest statement on this page of what -s false does: one directory read, no descent. Spec_TextFinder.md itself is absent from this list, and its absence is the rule rather than an omission: the pattern matches the file's own name in a citation, and the file cites its own name nowhere. These eight are the project-level documents, so this is the one invocation whose output does not move when any implementation's sources change. All three captures report the same eight paths in the same order, which makes this the one block a reader can compare across the three pages line for line.

3.7  Announcements and the Resolved Option Set

-h false adds a line for each file that matched nothing - the files 3.5 left silent - alongside the listing -v true writes ahead of the search.
7. /h false adds a line for each file that matched nothing - the files case 5 left
   silent - alongside the resolved option set from /v true.
  $ CSharp_TextFinder -P . -p "md, ixx, cpp, rs, cs" -r Spec_TextFinder\.md -h false -v true

      /P .
      /p md, ixx, cpp, rs, cs
      /r Spec_TextFinder\.md
      /s true
      /h false
      /v true
      /H false
      /n false
      /L false
      searched Constitution.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Cmdline/Prompts_Fix_Spec_Cpp_TextFinder_Cmdline.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Cmdline/Prompts_Spec_Cpp_TextFinder_Cmdline.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Cmdline/Spec_Cpp_TextFinder_Cmdline.md
      Cpp_Spec_driven_TextFinder/Cpp_Spec_driven_Cmdline/src/Cpp_TextFinder_Cmdline.ixx
      ... 77 more
      accessed 81 files, 36 directories

  91 line(s), exit 0
91 lines against 3.5's 61: nine listing lines plus 21 searched announcements added to the same 60 paths and the same summary. The listing's /r line carries the expression verbatim as typed, backslash included, and its /h and /v lines report the values that produced this output. The 21 announcements are exactly 3.2's 81 files less 3.5's 60, which is the arithmetic §3.4 promises: a file announcement reports only a file that produced no block, so the two sets partition the selected files and neither path appears twice. searched Constitution.md is the first line after the listing, which shows announcements interleaved with blocks in traversal order rather than collected at the end. This is the one block where that arithmetic can be checked against the program's own count rather than against another block. Under -h false every examined file appears exactly once, so the 60 paths plus the 21 announcements must equal the 81 the summary reports - and they do. The summary is not an announcement and takes no part in that partition; it reports the total the two sides add up to.

3.8  Two Roots

Two -P arguments, traversed in the order given. Each path begins with the root whose subtree holds it, per §3.4. This is the one invocation whose pattern names a construct of one language, so it is held to two C# directories rather than let loose on the tree.
8. Two roots, traversed in the order /P gave them. Each path begins with the root
   whose subtree holds it, and the skip list prunes bin/ and obj/ beneath both.
  $ CSharp_TextFinder -P CSharp_Spec_driven_TextFinder/CSharp_Spec_driven_Cmdline -P CSharp_Spec_driven_TextFinder/CSharp_Spec_driven_Output -p cs -r "^public " -n true -L true

      CSharp_Spec_driven_TextFinder/CSharp_Spec_driven_Cmdline/src/CommandLine.cs
        10 - public static class CommandLine
      CSharp_Spec_driven_TextFinder/CSharp_Spec_driven_Cmdline/src/ProgramCommands.cs
        9 - public sealed class ProgramCommands
      CSharp_Spec_driven_TextFinder/CSharp_Spec_driven_Output/src/StdoutSink.cs
        9 - public sealed class StdoutSink : IOutput, IDisposable
      accessed 5 files, 6 directories

  7 line(s), exit 0
Three path lines and three detail lines. The anchor ^ is what keeps the count that low: public appears many more times in those two files as a member modifier, each indented and so not at a line's start. What the three matches name is the public surface of two components - CommandLine, ProgramCommands, and StdoutSink - which the Cmdline and Output pages list in their Section 1. Each component's test/ folder sits beneath one of these two roots and neither run reports a file from it, because bin and obj are pruned and the suite sources themselves declare their types internal rather than public. That is the filter being a regular expression and nothing cleverer: the suite files were searched and matched nothing. accessed 5 files, 6 directories is the block that shows why the count is not a count of what was reported. Three files produced blocks and two did not - the two suite sources, hidden by the default /h true - so the file count exceeds the paths above it by exactly the files /h hid. Six directories for two roots is the src/ and test/ beneath each, plus the roots themselves; the bin/ and obj/ the skip list pruned are counted as none of them.

3.9  An Unopenable Root

A root path that does not exist is announced and the run still exits 0. The second root is a regular file, which §3.2 has searched as a single file rather than traversed.
9. A root path that cannot be opened is announced and the run still exits 0, while
   an error announcement ignores /h true.
  $ CSharp_TextFinder -P no_such_directory -P CSharp_Spec_driven_TextFinder/CSharp_TextFinder_Structure.md -r interface

      cannot open no_such_directory
      CSharp_Spec_driven_TextFinder/CSharp_TextFinder_Structure.md
      accessed 1 files, 0 directories

  3 line(s), exit 0
Three lines showing four rules. The announcement appears under the default /h true, because §3.4 gates file announcements on /h and error announcements on nothing. It reaches stdout rather than stderr, since CSharp_TextFinder_Dirnav emits it through the same IOutput as every block line, and the binary neither formats nor inspects it. And the exit code is 0: a root that could not be searched is a result, not a usage failure. The announcement came from a caught IOException rather than from a kind test. File.GetAttributes on a path that does not exist throws DirectoryNotFoundException, which derives from IOException, and the predicate that catches it is the same one that catches an unreadable directory mid-enumeration. The fourth rule is the summary's, and this is the smallest block that states it: accessed 1 files, 0 directories. The unopenable root counts as neither, its attributes never having been read, while the root that resolved to a regular file counts as one file and adds no directory. The uninflected 1 files is §3.6's accepted cost, visible here rather than argued for.

3.10  A Malformed Expression

-r "public (" is an unbalanced group that System.Text.RegularExpressions.Regex will not compile. §5.2 puts the option listing on stdout first, so the /r line shows what failed, then the diagnostic on stderr.
10. A malformed expression. §5.2 puts the option listing on stdout first, so the /r
    line shows what failed, then the diagnostic on stderr, and the exit code is 1.
  $ CSharp_TextFinder -P . -p "md, ixx, cpp, rs, cs" -r "public ("

      /P .
      /p md, ixx, cpp, rs, cs
      /r public (
      /s true
      /h true
      /v false
      /H false
      /n false
      /L false
      [stderr] invalid regex for switch: /r
      [stderr] usage: CSharp_TextFinder [/P path] [/p "ext, ext"] [/r regex] [/s bool] [/h bool] [/v bool] [/H bool] [/n bool] [/L bool]

  9 line(s), exit 1
Nine lines on stdout and exit 1. Three rules are visible at once: the listing appears whatever /v says, since the /v line itself reads false; the diagnostic is a reason line followed by the §5.1 usage line, with <executable> resolved to CSharp_TextFinder; and the listing reaches the stream ahead of the diagnostic, which the binary buys with an explicit Flush because the sink defers its writes. The framework's own RegexParseException message appears nowhere, and the specification gives the reason: that text belongs to the framework, so a runtime upgrade would alter this program's output with no document in this tree recording the change. What the user gets instead is the /r line above, carrying the expression as typed. The run traversed nothing. The expression is built at Dirnav construction, before the first root path is visited, so a bad pattern costs no file reads.

3.11  The Help Text

/H true writes the text §5.1 fixes to stdout and exits 0, traversing nothing. This block uses the / introducer where the others use -, which §4 makes equivalent.
11. The help text of §5.1, written to stdout under /H, traversing nothing.
  $ CSharp_TextFinder /H true

      usage: CSharp_TextFinder [/P path] [/p "ext, ext"] [/r regex] [/s bool] [/h bool] [/v bool] [/H bool] [/n bool] [/L bool]

        /P  path (.)             root path for traversal; repeat to add more root paths
        /p  "ext, ext" ()        comma-separated bare extensions to search; empty searches every file
        /r  regex (.)            regular expression evaluated against each line
        /s  true|false (true)    recurse into subdirectories
        /h  true|false (true)    hide files that matched nothing; errors always appear
        /v  true|false (false)   list the resolved option set before traversal
        /H  true|false (false)   print this help and exit
        /n  true|false (false)   add a detail line per match, carrying the line number
        /L  true|false (false)   add a detail line per match, carrying the line text

      A matching file prints its path on one line; /n and /L add indented detail
      lines beneath it. A path is never printed twice. A search ends with a line
      ... 8 more

  22 line(s), exit 0
22 lines, of which the first is the usage line invocation 10 sent to stderr. HelpText() is built from UsageLine(), so the two agree by construction rather than by proofreading, and the integration suite asserts both that this capture equals what the library returns and that it is 22 lines. It was 21 until §3.6 arrived. The block-form paragraph gained a sentence - "A search ends with a line counting the files and directories it reached." - and the assertion above moved with it, which is the cost of an assertion on fixed text and the reason it is worth making: a help text that drifted from §5.1 would fail rather than pass quietly. The excerpt cuts off mid-sentence at line 14, which is the 14-line rule doing its job rather than a truncated capture. This invocation traverses nothing, so it writes no run summary of its own. The eight withheld lines are the rest of that sentence and the three paragraphs on introducers, quoting, and the bare command line. §5.1 fixes every one of these 22 lines byte for byte with <executable> as the only substitution, so a C++ or Rust capture of this block differs from this one in exactly two words. Those 22 lines come from a string[] joined with "\n" rather than from a raw string literal, which is what keeps them independent of how the source file is stored.
demonstration complete

4.  Comparability

Page_Structure.md §7.4 states comparability against the part two runs share, and the cumulative extension list of §0 is what narrows that part. This run's five extensions admit all three implementations' sources, so this capture's path list contains both siblings' rather than diverging from them: every file the C++ or Rust run selects, this run selects too. Being third in the order, this is the widest record of the three and the one with the least left over to compare. The paths-agree claim holds only where two runs select the same files. §7.4 now says so outright, and the consequence is sharper than it sounds. It does not merely exempt the extra sources; it exempts the order of the shared documents too, because a path list is one depth-first walk and a Rust or C# source selected here interleaves with documents every run reports. Rust_Spec_driven_Cmdline/src/lib.rs falls between two documents the C++ capture reports consecutively, so the lists agree as sets over the documents and not as sequences. What follows is what survives that.
Comparable across implementations Is this run's own
Block 3.6's eight paths, in the same order, that invocation being the only one of the seven where two runs select the same files Every other invocation's path order, per §7.4
Block 3.1's nine listing lines and block 3.11's 22 help lines, but for the executable name §5.1 parameterizes Every file count but block 3.6's, since the file sets differ
Block 3.10's two stderr lines in full, but for that same name The header's executable and search root
The block form itself: the indent, the field separator, and the path written once The echoed command line's executable and extension list
The exit code of every invocation, §3.4 fixing all three values Block 3.8, whose pattern is a C# keyword
The form of every run summary: one line, two decimal counts, neither noun inflected The counts themselves, which include this implementation's own sources
Block 3.6's accessed 13 files, 1 directories, which all three report alike Blocks 3.8 and 3.9, rooted in this implementation's own directories
Block 3.6 is the one row of the left column that reports paths rather than fixed text, and it earns its place by searching the project root alone with -s false. No implementation's sources sit there, so the same eight documents in the same order appear in all three captures. A reader wanting to check §6's consistency guarantee against real output has that block and nothing else. One invocation of the seven escapes that limit, and §7.4 now names it as the only one. Block 3.6 reads the project root with -s false, where no implementation's sources sit, so every run selects the same 13 documents and reports the same eight paths in the same order. A reader wanting to check Spec_TextFinder.md §6's consistency guarantee against captured output has that block and no other. It would be easy to blame the cumulative list for that, and §7.4 separates the two things. Leaving 3.6 the only block whose whole path list agrees is not the cumulative list's doing: a per-language list would leave it the only one too, since two runs would still select different sources and still interleave them among the documents. What the cumulative list costs is narrower - it makes each run's file set a superset of the ones before it, so no two of the four pages report the same number of files. What it buys is the record. A per-language list would make this page a search of C# files alone - a fraction of the tree it is rooted at, and a shrinking fraction as languages are added. §0 takes the wider record and §7.4 states the narrower claim that record supports, rather than keeping a wider claim by narrowing what this page searches. Block 3.6 gained a second comparable row when §3.6 arrived, and it is the strongest one on the page. All three implementations report accessed 13 files, 1 directories there, because the invocation reads the project root alone and every implementation selects the same 13 documents in it. Two numbers agreeing across three implementations is a narrower claim than the eight paths agreeing, and a sharper one: the paths could agree while a count disagreed, and that would be a defect in whichever implementation counted differently. A mismatch in the comparable part is a defect in one of the implementations or an ambiguity in Spec_TextFinder.md, and the page reporting it says which. A disagreement in the exit code in particular is a defect rather than a difference of convention, since the three values are fixed rather than left to the implementation. Comparing two implementations means comparing captures taken over the same tree state on the same platform. §6 permits a line-for-line comparison only under that condition, because §3.2 leaves a directory's entries in filesystem order, and §6 scopes the guarantee to stdout, so stderr wording is each implementation's own. All three adopt §5.2's supplied wording unchanged, so block 3.10's stderr happens to agree as well.

5.  The Capture Is Dated

Seven of the 11 invocations count files in this project, so their output moves when a document is added to the tree or removed from it, and the run summary of §3.6 moves with it in each of the eight that traverse. This page therefore states the date of its capture, 2026-09-16, and a page whose counts disagree with a fresh run is stale rather than wrong. The summary sharpens that. Before §3.6, a reader comparing this page against a fresh run had to count a block's lines to notice the tree had moved; now every traversing block ends in two numbers that say so outright. This capture is the clearest case in the project of a count that moved for reasons the program had nothing to do with, because the thing that moved it is the thread this page belongs to. The Rust capture reports accessed 70 files for invocation 2; this one reports 81. Most of that difference is the C# implementation itself - four component specifications, its sources, its project files, its structure document, its prompt records - plus one extension added to the search. One of the 81 is this capture's own predecessor. CSharp_Spec_driven_TextFinder/Demo_9-16-2026.md was written from the run these blocks come from, so a demonstration of a search becomes a file that the next search finds - and because it quotes too large, it will lengthen invocations 3 and 4 the next time the runner is invoked. That is why §7.5 dates a capture rather than treating one as a fixed expected result. Invocation 6 is the one that did not move and will not: it searches the project root alone with -s false, where no implementation's files sit. A page rebuilt after the tree changes is recaptured in full rather than edited in place, since the counts appear both in the blocks and in the prose around them. Recapturing is one command:
run_demo.bat

6.  Source

Demo.cs in full. The 11 Case calls at the bottom are the 11 blocks above, and their note strings are the prose each capture carries. The driver is a console program rather than a test, which is a deliberate reading of §6.2: a demonstration shows what the program produces, and one that asserted its own output would be a fourth suite whose fixtures go stale every time a document joins the tree. It returns 0 unconditionally, and the runner reports that status.
demo/Demo.cs
// Demo.cs - runs CSharp_TextFinder against this project's own tree and shows what it
// produces, per Spec_TextFinder.md §6.2. It asserts nothing and fails nothing; its
// output moves as the tree changes, so a capture states the date it was taken.
//
// Run through run_demo.bat.

using System.Diagnostics;
using System.Globalization;
using System.Text;

namespace CSharp_TextFinder_Demo;

internal static class Demo
{
    private const string Extensions = "md, ixx, cpp, rs, cs";
    private const int Shown = 14;

    private static string _executable = string.Empty;
    private static string _projectRoot = string.Empty;

    private static string SolutionRoot()
    {
        DirectoryInfo? directory = new(AppContext.BaseDirectory);
        while (directory is not null && directory.Name != "CSharp_Spec_driven_TextFinder")
        {
            directory = directory.Parent;
        }

        if (directory is null) throw new InvalidOperationException("solution root not found");
        return directory.FullName;
    }

    private static string Forward(string path) => path.Replace('\\', '/');

    // The local date when run_demo.bat supplies it, the UTC civil date otherwise, so that
    // a capture dates itself however the demonstration was started.
    private static string Today()
    {
        string? supplied = Environment.GetEnvironmentVariable("TEXTFINDER_DEMO_DATE");
        if (!string.IsNullOrEmpty(supplied)) return supplied;
        return DateTime.UtcNow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
    }

    // Restores the quotes the shell removed, so the echoed command line can be retyped.
    private static string Quoted(string argument) =>
        argument.Contains(' ') || argument.Contains(',') ? "\"" + argument + "\"" : argument;

    private static void Case(int number, string[] note, params string[] args)
    {
        string indent = new(' ', number.ToString(CultureInfo.InvariantCulture).Length + 2);
        Console.WriteLine($"{number}. " + string.Join("\n" + indent, note));
        Console.WriteLine(("  $ CSharp_TextFinder " + string.Join(" ", args.Select(Quoted))).TrimEnd());
        Console.WriteLine();

        var start = new ProcessStartInfo(_executable)
        {
            WorkingDirectory = _projectRoot,
            RedirectStandardOutput = true,
            RedirectStandardError = true,
            UseShellExecute = false,
        };

        foreach (string argument in args) start.ArgumentList.Add(argument);

        using Process process = Process.Start(start)
                                ?? throw new InvalidOperationException("the executable could not be run");

        string stdout = process.StandardOutput.ReadToEnd();
        string stderr = process.StandardError.ReadToEnd();
        process.WaitForExit();

        var lines = new List<string>(
            stdout.Length == 0 ? Array.Empty<string>() : stdout.TrimEnd('\n').Split('\n'));
        int emitted = lines.Count;

        foreach (string line in stderr.Length == 0
                     ? Array.Empty<string>()
                     : stderr.TrimEnd('\n').Split('\n'))
        {
            lines.Add("[stderr] " + line);
        }

        foreach (string line in lines.Take(Shown))
        {
            Console.WriteLine(line.Length == 0 ? string.Empty : "      " + line);
        }

        if (lines.Count > Shown)
        {
            Console.WriteLine($"      ... {lines.Count - Shown} more");

            // Page_Structure.md §7.2 part 4: the run summary is the last line a traversing run
            // writes, so the excerpt above never reaches it. Show it rather than withhold it.
            if (emitted > Shown && lines[emitted - 1].StartsWith("accessed ", StringComparison.Ordinal))
            {
                Console.WriteLine("      " + lines[emitted - 1]);
            }
        }

        Console.WriteLine();
        Console.WriteLine($"  {emitted} line(s), exit {process.ExitCode}");
        Console.WriteLine();
    }

    private static int Main()
    {
        string solution = SolutionRoot();
        string name = OperatingSystem.IsWindows() ? "CSharp_TextFinder.exe" : "CSharp_TextFinder";
        _executable = Path.Combine(solution, "CSharp_Spec_driven_TextFinder_Entry",
            "bin", "Debug", "net8.0", name);
        _projectRoot = Directory.GetParent(solution)!.FullName;

        Console.OutputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

        Console.WriteLine();
        Console.WriteLine("CSharp_TextFinder demonstration");
        Console.WriteLine("  date:       " + Today());
        Console.WriteLine("  executable: " + Forward(_executable));
        Console.WriteLine("  root:       " + Forward(_projectRoot));
        Console.WriteLine("  extensions: \"" + Extensions + "\"");
        Console.WriteLine();

        Case(1, new[]
        {
            "No switch at all. The command line names no work, so CSharp_TextFinder lists",
            "the options a real invocation would start from and exits 0 (Spec_TextFinder.md §3.1).",
        });

        Case(2, new[]
        {
            "Default expression. The default /r of . with no /n or /L needs no file content,",
            "so each selected file is reported by its path line alone (Spec_TextFinder.md §3.3).",
        }, "-P", ".", "-p", Extensions);

        Case(3, new[]
        {
            "The two-level block of §3.4: a path written once, then an indented detail line",
            "per match carrying the line number and the line's text.",
        }, "-P", ".", "-p", Extensions, "-r", "too large", "-n", "true", "-L", "true");

        Case(4, new[]
        {
            "The same search with /L false, leaving the line number alone on each detail line.",
        }, "-P", ".", "-p", Extensions, "-r", "too large", "-n", "true");

        Case(5, new[]
        {
            "Which documents cite the parent specification. Neither /n nor /L, so every block",
            "is its path line and no path is written twice.",
        }, "-P", ".", "-p", Extensions, "-r", "Spec_TextFinder\\.md");

        Case(6, new[]
        {
            "The same search one level deep, /s false entering no subdirectory.",
        }, "-P", ".", "-p", Extensions, "-r", "Spec_TextFinder\\.md", "-s", "false");

        Case(7, new[]
        {
            "/h false adds a line for each file that matched nothing - the files case 5 left",
            "silent - alongside the resolved option set from /v true.",
        }, "-P", ".", "-p", Extensions, "-r", "Spec_TextFinder\\.md", "-h", "false", "-v", "true");

        Case(8, new[]
        {
            "Two roots, traversed in the order /P gave them. Each path begins with the root",
            "whose subtree holds it, and the skip list prunes bin/ and obj/ beneath both.",
        }, "-P", "CSharp_Spec_driven_TextFinder/CSharp_Spec_driven_Cmdline",
           "-P", "CSharp_Spec_driven_TextFinder/CSharp_Spec_driven_Output",
           "-p", "cs", "-r", "^public ", "-n", "true", "-L", "true");

        Case(9, new[]
        {
            "A root path that cannot be opened is announced and the run still exits 0, while",
            "an error announcement ignores /h true.",
        }, "-P", "no_such_directory",
           "-P", "CSharp_Spec_driven_TextFinder/CSharp_TextFinder_Structure.md", "-r", "interface");

        Case(10, new[]
        {
            "A malformed expression. §5.2 puts the option listing on stdout first, so the /r",
            "line shows what failed, then the diagnostic on stderr, and the exit code is 1.",
        }, "-P", ".", "-p", Extensions, "-r", "public (");

        Case(11, new[]
        {
            "The help text of §5.1, written to stdout under /H, traversing nothing.",
        }, "/H", "true");

        Console.WriteLine("demonstration complete");
        return 0;
    }
}
Quoted restores the quotes the shell removed, so every echoed command line can be retyped as it appears. Without it block 3.2's command would read -p md, ixx, cpp, rs, cs, which a shell would hand the program as five arguments and which would draw not a switch: ixx,. Case counts stdout lines before appending the stderr lines, which is why block 3.10 reports 9 while showing 11. The count §7.2 asks for is of the run's stdout, and the [stderr] prefix keeps a diagnostic distinguishable from a record without reordering either. Console.OutputEncoding is set to a UTF8Encoding that emits no byte-order mark, so the driver's own output carries the same bytes the executable's does. That is the driver reaching for the framework's Console where the sink under test refuses to - and the difference is the point: the driver prints prose for a reader, and the sink writes output §3.4 fixes to the byte.

7.  Prompt Records

This page carries none. Page_Structure.md §8 gives its last row to any record of demonstration changes made at the language level, stated by role rather than by name, and this thread produced one record covering every turn, which sits on the Process page.