Spec-Driven

Spec-Driven Process: Writing the Project Specification

how Spec_TextFinder.md reached its form, and what makes it testable

Synopsis:
This page documents Spec_TextFinder.md, the language-neutral specification every implementation derives from. It covers how the document reached its form, not what it requires.
  • The first draft answered a four-sentence prompt and guessed at four open points. Each guess was flagged rather than buried, and three were later overridden.
  • Three kinds of turn moved it since - prompted additions, review passes that returned ambiguities instead of edits, and rules promoted up out of a language spec.
Four properties make the document checkable by test rather than by reading.
  • Every requirement is observable at the process boundary - stdout, stderr, an exit code.
  • Quantities appear as numbers, so each is one test case rather than a judgment call.
  • Fixed text is quoted in full, not described, so two implementations cannot drift apart in wording they were handed.
  • Accepted costs are stated, including the two the design gives up for speed and for filesystem ordering.
The record shows the document tightening and, twice, retreating.
  • An 18-item review and a later 15-item cross-document pass, answered item by item.
  • Section 4 carries the two prompt conversations in full.

1.  The Specification as an Artifact

Spec_TextFinder.md fixes behavior common to every implementation, in no particular language. It runs seven sections: purpose, scope, functional requirements, command-line syntax, defined switches, non-functional requirements, and non-goals. What it requires is the subject of the Behavior thread. This page covers how it came to say that. The document did not start where it ended. The first draft answered a four-sentence prompt and guessed at four things the prompt left open: which switch letters to use, whether recursion defaults on or off, a grep-style path:line:text output form, and whether symbolic links and binary files were in scope. Each guess was flagged in the reply rather than buried, and three of the four were later overridden.

2.  How It Reached Its Current Form

Three kinds of turn moved the document, and the records show all three.

2.1  Prompted Additions

The skip list arrived this way: the user asked for a list of directories never entered during traversal, named five examples, and asked for a function that adds a custom name. That one prompt produced three edits - the skip-list concept and its 11 default names in §3.2, addSkipDirectory(name) in §3.5, and a command-line route to it. The command-line route was withdrawn a turn later, when the switch set was fixed and the list read as exhaustive; extension of the skip list has been build-time only since. The switch set arrived the same way and cost two turns. The user's first message named /P for the root path and /p for extensions, which collided with the draft's /p for the regular expression. Rather than guess, the reply stopped and offered three ways out; the user answered with the eight switches and their defaults verbatim, and the table was replaced wholesale.

2.2  Review Passes

Twice the user asked for ambiguities rather than for changes. The first pass returned 18 items grouped by severity - nine real ambiguities that could produce diverging implementations, three terminology and structural items, six smaller ones - and made no edits. The user answered all 18 in one message, item by item, and the answers were applied in one pass. Four of those answers still show in the current text. Answer 2 set the encoding to UTF-8. Answer 3 asked for a line delimiter covering Windows, Linux, and macOS, which became the three terminators of §3.3 with the unterminated final line treated as a line. Answer 4 named ECMAScript and asked that the regex machine be built once per run. Answer 9 fixed emission order as "as they occur", which is why §3.4 says a block's path line is written the moment the first match is found and nothing is buffered to the end of a file. The second pass ran after all five specifications existed and found 15 items, four of them contradictions between documents that each read correctly alone. Those are covered on the C++ Process page, since the conversation sat at the C++ level.

2.3  Promotion from a Language Spec

The third kind of turn moves a rule up. While the C++ Dirnav specification was under review, 13 items came back, and the user's closing line was "It looks like all of these belong in the Project spec." Ten of the thirteen went into Spec_TextFinder.md, because each changes emitted output and so binds all four implementations: symbolic-link and non-file root handling in §3.2; the three admission tests in §3.3; path rendering and the announcement table in §3.4; the dot-file rule and extension case-sensitivity in §5; and the NUL test in §7. The test is mechanical: if a rule changes what a user sees, it belongs where it binds everyone. The C++ specification kept only the bindings - which standard-library call produces that behavior, and which one looks right and is wrong.

3.  Four Properties That Make It Testable

The specification is written to be checked by test rather than by reading. Four properties do most of that work.

3.1  Requirements Are Observable at the Boundary

Every requirement in §3 through §5 is stated over something a test can see from outside the process: a line on stdout, a line on stderr, an exit code, a file opened or not opened. §3.4 fixes the three exit codes rather than leaving them to the implementation, with the reason given in the document: so that two implementations can be compared by exit code as well as by output. The same discipline shows in what the specification declines to fix. §3.4 governs the stdout terminator and says plainly that the stderr terminator is whatever the runtime and platform produce, because holding stderr to the byte would test the platform rather than the implementation. A rule that cannot be checked is not stated as a rule.

3.2  Quantities Appear as Numbers

Each of these is one test case rather than a judgment call.
Quantity Value Where
File size limit 10,485,760 bytes §3.3, as 10 MB and as the byte count
Default skip-list names 11 §3.2, listed in full
Defined switches 9 §5 table
Line terminators recognized 3 §3.3: LF, CRLF, bare CR
Exit codes 3 §3.4, values fixed
Usage diagnostics 7 §5.2, reason lines quoted
Option-listing lines 9 §5.3, the example reproduced
Detail-line indent 2 spaces §3.4, "exactly two spaces"
The size limit shows what the precision buys. §3.3 gives both the round figure and the byte count, so the C++ unit suite can test a file exactly at the limit and a file one byte above it and expect a different outcome from each.

3.3  Fixed Text Is Quoted, Not Described

§5.1 does not describe the help text; it reproduces it, 22 lines, with <executable> as the one substitution an implementation makes. §5.2 does not describe the diagnostics; it tables the seven reason lines with <token> and <switch> as the substitutions. §5.3 does not describe the option listing; it shows the nine lines a bare -v true produces. Two implementations cannot drift apart in wording they were handed. The alternative was tried: the help text lived in the C++ command-line specification for several turns, where it looked like cross-language determinism but bound one language. Moving it to the parent was the point of one turn, and the diagnostics followed in the next when the user asked to make them byte-identical. Two of those three are still binding, and one is not. §5.1's help text and §5.3's listing reach stdout, so the specification fixes them. The §5.2 reason lines reach stderr, and a later pass demoted them: they are now the wording the specification supplies, which a language may replace in its own idiom, with each implementation's component specification owning what it emits. The quoting is unchanged; what changed is who the quoted text binds. That retreat is the same move Section 3.4 describes and worth noticing here, because it ran in the opposite direction from the one above. Fixing the help text tightened the specification and cost nothing. Fixing the diagnostics tightened it past what the project could keep, since §3.4 had always left the stderr line terminator to the platform, so "identical across implementations" was never checkable across platforms. §5.2 now binds the shape, the destination, the exit code, and what each refusal leaves on stdout, and leaves the words alone. All three implementations built so far adopt the supplied wording anyway. The quoted text carries its own fine print, and that is deliberate too. §5.3 notes that an empty /p list emits /p alone, with no separating space and no trailing space, "so that no line of the listing ends in whitespace" - and gives the reason: a fixture holding this text survives an editor that strips trailing space. A rule with no stated reason gets edited away by someone tidying up.

3.4  Accepted Costs Are Stated

Two places in the document name what the design gives up rather than claiming it gives up nothing.
  • §3.3's no-content case. With the default expression and neither /n nor /L, a block is its path line alone, so TextFinder reports every selected non-empty file without opening it. The section states both consequences it accepts for that speed: a file the content tests would have rejected, a binary file among them, is reported; and so is a file whose lines are all empty, which . would not in fact have matched.
  • §6's ordering limit. Because §3.2 leaves a directory's entries in filesystem order, the total emission order is reproducible only across runs over the same tree on the same platform and filesystem. Elsewhere the match set still agrees and the order of matches from different directory entries may not.
The second of those is a retreat the records show being made. An earlier version required sorted sibling order, which delivered a stronger guarantee. The user objected that depth-first traversal should visit entries in the order the standard library presents them, and the reply agreed on the definition, corrected an overstated risk from the earlier review, and named the three places that had to change together so the document would not claim what it no longer delivered: §3.2, §6, and the C++ Dirnav spec's traversal rule. All three changed in the next turn. §6.1 is the largest instance of the same habit. No regex engine spans the four languages, so the section names one engine per language, fixes the pattern subset all four accept alike, and then states the two costs of a pattern outside it - including that one implementation may refuse a command line the other three run. That is covered on the Contracts page.

4.  Prompt Records

The two conversations that produced Spec_TextFinder.md, in full. The first covers the initial draft through the switch set; the second covers the 18-item review, the 18 decisions, and the last-wins rule for a repeated switch.
Prompts_Spec_TextFinder.md
# Prompts and Replies — Spec_TextFinder Development

This file records the prompts and replies exchanged while drafting [Spec_TextFinder.md](Spec_TextFinder.md).

---

## Turn 1

### User

`/init` — asked Claude Code to analyze the codebase and create a CLAUDE.md file for future instances of Claude Code working in this repository.

### Assistant

The project directory `c:\github\JimFawcett\NewSite\Code\Projects\Spec_driven_TextFinder` was empty — no code, no existing CLAUDE.md. Rather than fabricate architecture details for code that did not exist yet, I offered three options:

1. **Wait until code exists** — CLAUDE.md is most useful when there's actual architecture to document.
2. **Create a minimal starter** referencing the parent NewSite/CLAUDE.md and noting this is a spec-driven project with details to be filled in as it develops.
3. **You tell me the intent** — what language(s), what the spec-driven workflow looks like, whether it's a comparison thread like the Rust/C++ textfinders in your recent commits.

Asked which the user preferred, and whether this was meant to be a third variant driven from a written spec.

---

## Turn 2

### User

> create Spec_TextFinder.md. This project will build TextFinders in C++, Rust, C#, and Python. For now, we concentrate on C++ only. TextFinder is a command line tool that recursively visits directories and files looking for a match to a supplied RegEx. All matches are sent to stdout. The command line accepts commands in the form /ch or equivalently -ch, where ch is a case sensitive single character. This Spec_Driven_TextFinder directory has child folders for Cpp_TextFinder and, eventually for Rust_TextFinder, CSharp_TextFinder, and Python_TextFinder. Do not use any code outside Spec_Driven_TextFinder as examples. This is intended to be entirely specification driven. Each language child folder will contain a Spec_*.md and Structure_*.md. This folder will contain a Constitution_TextFinder.md.

### Assistant

Created [Spec_TextFinder.md](Spec_TextFinder.md) with sections for purpose, scope, functional requirements (input, traversal, matching, output), command-line syntax, a defined-switch table, non-functional requirements, non-goals, and development order.

Flagged four judgement calls made in the draft:

- **Switch letters** — proposed `/p /d /e /s /v /n /h`. The user specified the form (`/ch` or `-ch`, case-sensitive single letter) but not which letters. Invited adjustment.
- **Recursion default** — recursion enabled by default with `/s` suppressing it, on the reading that "recursively visits directories" describes normal operation. Offered to invert if `/r` should opt into recursion.
- **Output format** — grep-style `path:line:text`. Offered alternatives.
- **Symlinks / binary files** — both marked as non-goals to keep behavior deterministic across the four language implementations.

Noted the next likely files: Constitution_TextFinder.md at this level, then Cpp_TextFinder/Spec_Cpp_TextFinder.md and Cpp_TextFinder/Structure_Cpp_TextFinder.md.

---

## Turn 3

### User

> add to traversal a skip list, e.g., a list of directories that are not entered while searching. These include archive, .git, .gitignore, build, and any other directories that hold intermediate build files. The public interface needs a function to add a custom skip directory name.

### Assistant

Made three edits to [Spec_TextFinder.md](Spec_TextFinder.md):

- **§3.2 Traversal** — added the skip-list concept, a default list (`archive`, `.git`, `.svn`, `.hg`, `build`, `out`, `target`, `bin`, `obj`, `__pycache__`, `node_modules`), and case-sensitivity rules that follow the host filesystem.
- **§3.5 Public Interface** — new section introducing `addSkipDirectory(name)`, with per-language naming and typing deferred to each language's Spec_*.md.
- **§5 switch table** — added `/x <dirname>` (repeatable) as the command-line route to the same skip-list extension.

Flagged two judgement calls:

- `.gitignore` is a file, not a directory, so it was left out of the default skip list. Offered a separate file-name skip mechanism if wanted (`/X` for repeatable file-name exclusion).
- The default set was expanded to include common metadata dirs (`.svn`, `.hg`) and per-language build dirs (`target`, `bin`, `obj`, `__pycache__`, `node_modules`) to honor the phrase "any other directories that hold intermediate build files." Invited trim.

---

## Turn 4

### User

> Command line commands should be: /P . : start path, /p "" : file extension patterns to search of the form /p "cpp, rs, ...

### Assistant

Recognized a letter collision: my earlier `/p` was the regex pattern, the new `/p` is the extensions list. Read [Spec_TextFinder.md](Spec_TextFinder.md) to see the current text before editing.

---

## Turn 5

### User

> stop

### Assistant

Stopped before making any edits — only a Read had been performed this turn. Confirmed the file still showed `/p` for regex, `/d` for path, `/e` for extensions.

Offered three options for handling the collision:

1. Move regex to `/r` (or another letter) and apply `/P` for path, `/p` for extensions.
2. Make the regex a positional argument and drop it from the switch table.
3. Something else.

---

## Turn 6

### User

> Here are the commands I want:
> /P . : starting path (default .)
> /p "cpp, rs, ... : file extension patterns to search
> /r "." : regular expression
> /s true : recursive search
> /h true : do not announce files with no match
> /v false : display these options at top
> /H false : display help text
> /n true : display text line number
> Values given are defaults. Omitting command uses default value

### Assistant

Applied the new switch model to [Spec_TextFinder.md](Spec_TextFinder.md):

- **§3.1 Input** — reframed: every switch has a default; bare command line runs with defaults.
- **§3.2 Traversal** — `/s false` disables recursion; extension filtering attributed to `/p`; `/x` reference removed (skip-list extension is programmatic only via §3.5).
- **§4 Command-Line Syntax** — every switch takes one argument; booleans are `true`/`false`; quoted-string examples for `/p` and `/r`.
- **§5 Switch table** — replaced with the eight specified switches: `/P /p /r /s /h /v /H /n` with defaults `.`, `""`, `"."`, `true`, `true`, `false`, `false`, `true`.
- **§7 Non-Goals** — binary-file diagnostic tied to `/h false` (was `/v`).

Left two open items for confirmation:

1. **Skip list CLI access** — dropped `/x` since the list read as exhaustive. `addSkipDirectory` (§3.5) still exists. Confirm whether the CLI route should return under a different letter, or programmatic-only is correct.
2. **`/H` handling** — wrote "when `true`, print help and exit; all other switches are ignored." Confirm.

---

## Turn 7

### User

> create Prompts_Spec_Textfinder.md containing all the prompts and replies in this conversation

### Assistant

Created this file, [Prompts_Spec_Textfinder.md](Prompts_Spec_Textfinder.md), summarizing each prompt and reply in the conversation. User prompts are quoted verbatim where they were user-authored text. Assistant replies are summarized from the actual responses; tool calls (Read/Edit/Write) are noted as effects rather than transcribed.
Prompts_Fix_Spec_TextFinder.md
# Prompts and Replies — Fixes to Spec_TextFinder

This file continues the record begun in [Prompts_Spec_TextFinder.md](Prompts_Spec_TextFinder.md). Turns numbered here start from Turn 8, immediately after that file was created.

---

## Turn 8

### User

> are there ambiguities or changes that would improve the Spec_TextFinder.md. Don't make any changes without my authorization

### Assistant

Re-read [Spec_TextFinder.md](Spec_TextFinder.md) and returned an 18-item review, grouped by severity, with no file changes.

**Real ambiguities (could produce diverging implementations):**

1. Windows path collision in the `<path>:<lineNumber>:<matchedLine>` output form — Windows paths contain `:` and cannot be split reliably.
2. Text encoding not specified in §3.3 — the largest source of implementation drift across C++, Rust, C#, and Python defaults.
3. Line delimiter not specified — LF only, CRLF, both? Final unterminated line?
4. Regex dialect not normative — §6 acknowledged differences but named no baseline.
5. Extension semantics undefined for `foo.tar.gz`, `Makefile`, or `.gitignore`.
6. Skip-list match target — basename or full path? Not explicit.
7. Duplicate, unknown, or malformed switches — behavior undefined.
8. Boolean literal case — is `True` accepted, or only `true`?
9. Match ordering across implementations — same set required but same order?

**Terminology / structural:**

10. Three phrases used for the same concept: "root path" (§3.2), "starting path" (§3.1), "current directory" (§5).
11. §1 "recursively traverses" is stronger than the actual semantics now that /s can disable recursion.
12. §3.5 title "Public Interface" is broader than its single-function content.

**Smaller items:**

13. Path form for /P — absolute, relative, or either?
14. Which quote character(s) are recognized?
15. /v output format not specified.
16. Exit-code coverage doesn't address per-file read failures or root-is-a-file.
17. Root-is-a-file case — single-file search or error?
18. /H help exit code not stated.

---

## Turn 9

### User

> 1. change seperator to " - ". Make the display of the matchedLine optional with option /L false.
> 2. set encoding to UTF-8.
> 3. choose a line delimiter that works with Windows, Linux, and MacOS. Final unterminated line is treated like a line.
> 4. Use ECMAScript. Also, where appropriate required the regex machine to be evaluated only once for each run of any of the TextFinders.
> 5. last dot-sufix. Search extension-less files only if no patterns are specified.
> 6. say so explicitly
> 7. a and b must be relative paths, all others are errors
> 8. make case-insensitive
> 9. list matches as they occur, so that will be directory traversal order. "as they occur" is the definitive spec, e.g., sent to output as soon as evaluated.
> 10. Root path
> 11. adopt your suggestion
> 12. leave as is
> 13. either
> 14. use double quotes
> 15. one key/value per line
> 16. That is left to the textfinder parts, e.g., DirNav, ...
> 17. treat as single-file search
> 18 Exit code 0

### Assistant

Applied all 18 answers to [Spec_TextFinder.md](Spec_TextFinder.md):

- **§1** — softened "recursively traverses" to "recursively by default."
- **§3.2** — adopted "root path" terminology; added single-file handling for /P; multi-/P accumulates root paths in the order given; skip-list explicitly compares against directory basename.
- **§3.3** — content read as UTF-8; a line is a maximal run bounded by LF (U+000A), CRLF, or bare CR; unterminated final line is a line; regex syntax is ECMAScript; expression compiled exactly once per invocation.
- **§3.4** — separator changed to ` - ` (space-hyphen-space); full form `<path> - <lineNumber> - <matchedLine>`; `/L` controls emission of the matched-line field; `/n` controls the line-number field; both false → path-only; matches emitted as they occur (streamed, not buffered); `/H true` exits 0 with help on stdout.
- **§4** — boolean values matched case-insensitively (`TRUE`, `True`, `true` all valid); only double quotes are recognized as quoting characters; unrecognized switch, malformed boolean, and missing following-token all produce a stderr diagnostic and non-zero exit.
- **§5 table** — updated /P, /p, /r, /s, /v, /H, /n; added new /L row (default `true`).
- **§6** — ECMAScript dialect fixed; same match set and same order required across implementations.
- **§7** — binary-skip criterion is UTF-8 decode failure.

Flagged one interpretation choice:

- Answer #7 said `a and b must be relative paths, all others are errors`, while answer #13 said `/P` path form is "either." Read #7's "must be relative paths" as an affirmation that the two example values in the duplicate-`/P` case were fine (and duplicates are allowed and accumulate), not as a rule constraining every `/P` value. `/P` row now reads: "may be an absolute or a relative path" and "may be given more than once." Invited correction if the user meant `/P` values must be relative in all cases.

Also raised one item not covered by the 18 answers: duplicate handling for non-`/P` boolean/value switches (e.g., `/s true /s false`). Asked whether to make it last-wins, first-wins, or error.

---

## Turn 10

### User

> last-wins

### Assistant

Added a sentence to §4 of [Spec_TextFinder.md](Spec_TextFinder.md):

> When a switch other than /P appears more than once on the command line, the last occurrence wins; earlier occurrences are silently discarded. (/P accumulates root paths as described in §5.)

The parenthetical preserves the `/P` exception documented in §5.

---

## Turn 11

### User

> create Prompts_Fix_Spec_TextFinder.md with the prompts and responses that follow those written to Prompts_Spec_TextFinder.md

### Assistant

Created this file, [Prompts_Fix_Spec_TextFinder.md](Prompts_Fix_Spec_TextFinder.md), following the same format as [Prompts_Spec_TextFinder.md](Prompts_Spec_TextFinder.md): user prompts quoted verbatim, assistant replies condensed to record the substance of what was said and the edits made, rather than reproduced word-for-word.