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.