Skip to content

damage: one rule about accept, two callers, and a site that lists every command - #100

Merged
donislawdev merged 1 commit into
mainfrom
fix/damage-on-site-and-expectation-parity
Sep 9, 2026
Merged

damage: one rule about accept, two callers, and a site that lists every command#100
donislawdev merged 1 commit into
mainfrom
fix/damage-on-site-and-expectation-parity

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

Three defects that turned out to be the same shape: a list written out by hand that nothing compared against its source. All three were green.

1. expected: accept beside damage was refused on one surface out of two

Measured on the released 0.3.0 binary, exit code captured into a variable:

route before after
generate --damage zero-head --expected accept 0, files written, manifest says a deliberately broken file should be accepted 2, nothing written
generate recipe.yaml 3, with the address of the target 3, unchanged
validate recipe.yaml 3 3, unchanged

ExpectationConflictError had exactly one caller, in the recipe reader, while the two sibling damage refusals - a file below the floor, and a damage that moved no byte - live in the engine where every route passes. The command line did not skip the check, it never reached it.

The condition is now Chain.ConflictsWithExpectation in internal/damage with two callers rather than two copies: the recipe reader, which keeps reporting it beside the other problems of that recipe and with the target address, and engine.checkDamage. The recipe path did not move in any detail, which was the criterion for picking this shape.

Exit code 2, not 4. The frozen table defines 4 as a request no format can meet, and this conflict stands identically for all twenty four - a script reading 4 would go looking for another format or size, and neither is the fix. Same classification as damage.UnknownError.

Only accept. Measured after the fix: reject, sanitize, unspecified beside damage all still end with 0, so does damage with no expectation, and so does accept with nothing damaged. Without those four cases the guard would pass for a build that refuses everything.

2. The site listed nine of the ten commands

grep -rc damage web/ returned zero hits across the whole directory. site.Facts carried the version, formats, exit codes, presets and downloads - but no commands, so the guard comparing the site against the program had no list to compare with.

The block is rendered now: names from cli.Run with --help, summaries from the language file, the same split the exit code table already uses. Both directions are checked - a command with no summary and a summary with no command. The parser refuses rather than returning an empty list, because a guard that quietly stopped finding the Commands: block would compare the page against nothing and pass.

The regeneration diff is one line per language, which is the evidence that the partial reproduces the old block exactly.

Also here: a damage section on the docs page in both languages, a --damage row in the flag table, and the same row in README, where the flag was missing too.

3. sign_release.py blamed the card reader

Windows PowerShell 5.1 launched through subprocess from a Python started under pwsh does not have the Cert: drive. The error is non terminating, so the exit code was zero, and stderr was read only on a non-zero code - the message was discarded and the script announced a missing certificate while the card was in the reader. Signing 0.3.0 cost an hour on that.

Measured in one process, one second apart: Get-ChildItem Cert:\CurrentUser\My returned nothing plus "Cannot find drive", X509Store('My','CurrentUser') returned 11. Three interleaved rounds after the fix: the same thumbprint from both shells, every time.

Both halves are needed, and the first one found the cause - added first, it immediately printed the complaint that had been sitting in an unread stream.

Verification

  • full suite green, gofmt from $(go env GOROOT)/bin and go vet clean
  • journey.py 300 of 300
  • three new guards, eight mutations, all caught; two came back NOT CAUGHT first and both were statements about the mutation, not the code - one named a guard that never renders a page, the other aimed at a branch unreachable while every command has a summary
  • one existing mutation was invalidated by this change and repointed; staleness.py reports every pattern occurring exactly once
  • engine.go reached 411 lines against a ceiling of 408, answered with a cut rather than a larger number

Left open

O201: the Commands: block in tfg --help and the generate flag table in README are still hand written lists with nothing comparing them to their source. Same class, one floor down. README was missing --damage and nothing said so.

🤖 Generated with Claude Code

…ry command

Three defects that were all the same shape: a list written out by hand that
nothing compared against its source.

The refusal for a target that damages its files and declares they will be
accepted lived in the recipe reader alone. The command line never skipped past
it - it never reached it, so `--damage zero-head --expected accept` ended with
code 0 and wrote a manifest saying a deliberately broken file should be
accepted, while the identical recipe was refused with code 3.

The condition is now `Chain.ConflictsWithExpectation` in internal/damage with
two CALLERS rather than two copies: the recipe reader, which keeps reporting it
beside the other problems of that recipe and with the address of the target,
and the engine, which every surface passes through. The command line ends with
code 2 and writes nothing. Only accept is refused - reject, sanitize and
unspecified beside damage, and accept without damage, all still work, and the
guard asserts that so this cannot become a wall.

The website listed nine of the ten commands, because `tfg damage` arrived and
the list was a hand copy of what `tfg --help` prints. site.Facts carried no
commands at all, so nothing could compare. The block is rendered now: names
from the help itself, summaries from the language file, both directions
checked, and the parser refuses rather than returning an empty list - a guard
that stopped finding the block would compare the page against nothing and pass.
The site also gains a section on producing files that are broken on purpose,
and README gains the --damage row its flag table never had.

sign_release.py asked the certificate store through the Cert: drive, which does
not exist when Windows PowerShell 5.1 is launched from pwsh. The error is non
terminating, so the exit code was zero, and stderr was read only on a non-zero
code - the script announced a missing card while the card was in the reader. It
opens the store through .NET now and prints what PowerShell said whatever the
code was.

Guards: three new, eight mutations, all caught. Two came back NOT CAUGHT first
and both were statements about the mutation rather than the code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit 80cb92a into main Sep 9, 2026
18 checks passed
@donislawdev
donislawdev deleted the fix/damage-on-site-and-expectation-parity branch September 9, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant