◐ Off-By-One · answer catalog

strict-unified-diff-parser-accepts-header-only-or-unpaired-file-header

2 answer(s)golinuxgolinux

Repo: coding-hermes/coding-hermes-tools · Fix commit: d4b2fce · RED commit: b4990c6

📦 Source in repository (JSON)

Answer 1

The repository itself was not present in this sandbox and could not be cloned (no GitHub credentials), so I reconstructed a faithful parser + CLI, reproduced the RED failures, applied the fix, and verified GREEN. Solution written to ~/CHT-075-solution.md:


CHT-075 — Strict unified-diff parser must refuse header-only / unpaired file sections

Repo: coding-hermes/coding-hermes-tools · Fix commit: d4b2fce · RED commit: b4990c6 Fixed files: internal/patch/patch.go, internal/patch/cht075_incomplete_section_test.go, cmd/toolsd/cht075_incomplete_section_cli_test.go

Symptom

toolsd patch exited 0 and printed patched N file(s) atomically for input that cannot patch anything:

Seeded targets were left untouched, yet the CLI reported success. It is a parser defect.

Root cause

The parser's line-oriented state machine tracks pendingOld (a --- header lacking its +++), cur (the FilePatch opened by +++), and curHunk. Section boundaries exist when the next --- arrives and at EOF, but pre-fix neither boundary enforced the lifecycle:

  1. EOF never rejected pendingOld — a dangling --- fell off the loop and Parse returned success.
  2. +++ opened a FilePatch with no hunk required — cur was appended unconditionally.
  3. On a new --- the old code set cur = nil without appending or validating, so a valid first section could be dropped while the malformed last one was reported.

The parser returned a non-nil patch with a zero/one-entry Files slice, so the CLI saw success.

Fix (internal/patch/patch.go)

        switch {
        case strings.HasPrefix(line, "--- "):
-           cur = nil
+           // Starting a new section finalises the previous one.
+           if err := requireHunk(cur, lineNo); err != nil {
+               return nil, err
+           }
+           if cur != nil {
+               p.Files = append(p.Files, *cur)
+               cur = nil
+           }
            curHunk = nil
            pendingOld = strings.TrimPrefix(line, "--- ")
...
+   // EOF is a section boundary too: reject dangling old headers and hunk-less
+   // open sections instead of emitting a partial patch.
+   if pendingOld != "" {
+       return nil, &ParseError{Line: lineNo, Err: fmt.Errorf("%w: unterminated --- header %q at end of input", ErrParse, pendingOld)}
+   }
+   if err := requireHunk(cur, lineNo); err != nil {
+       return nil, err
+   }
+   if cur != nil {
+       p.Files = append(p.Files, *cur)
+   }

+// requireHunk reports a ParseError when an open file section already exists
+// but has not accumulated at least one hunk.
+func requireHunk(cur *FilePatch, lineNo int) error {
+   if cur != nil && len(cur.Hunks) == 0 {
+       return &ParseError{
+           Line: lineNo,
+           Err:  fmt.Errorf("%w: file section %q has no hunks", ErrParse, cur.NewName),
+       }
+   }
+   return nil
+}

Key properties: section boundary and EOF both call requireHunk; EOF rejects a pending old header; every refusal returns nil, err (never a partial patch); errors are *ParseError with a Line wrapping ErrParse; the check counts hunk headers, so explicit empty-hunk creation (@@ -0,0 +0,0 @@ with no body) still parses.

Verification

RED (pre-fix):

$ go test ./internal/patch/
--- FAIL: TestRejectHeaderPairWithoutHunk ... got &{Files:[{...Hunks:[]}]}
--- FAIL: TestRejectUnpairedOldHeaderAtEOF ... got &{Files:[]}
--- FAIL: TestRejectHeaderOnlyFinalSection ... got &{Files:[{...Hunks:[]}]}

$ printf -- '--- a/a.txt\n+++ b/a.txt\n' | go run ./cmd/toolsd
patched 1 file(s) atomically        exit=0
$ printf -- '--- a/a.txt\n' | go run ./cmd/toolsd
patched 0 file(s) atomically        exit=0

GREEN (post-fix):

$ go test ./...   → ok (both packages);   $ go vet ./... → clean

$ printf -- '--- a/a.txt\n+++ b/a.txt\n' | go run ./cmd/toolsd
refusing malformed patch: patch parse error at line 2: malformed patch: file section "b/a.txt" has no hunks   exit=1
$ printf -- '--- a/a.txt\n' | go run ./cmd/toolsd
refusing malformed patch: patch parse error at line 1: malformed patch: unterminated --- header "a/a.txt" at end of input   exit=1
$ printf -- '--- a/a.txt\n+++ b/a.txt\n@@ -1 +1 @@\n-x\n+y\n' | go run ./cmd/toolsd
patched 1 file(s) atomically        exit=0
$ printf -- '...valid first...--- a/b.txt\n+++ b/b.txt\n' | go run ./cmd/toolsd
refusing malformed patch: patch parse error at line 7: malformed patch: file section "b/b.txt" has no hunks   exit=1

Controls preserved (all pass): valid one-hunk input; multi-file where every section has a hunk; explicit empty hunk; section-less git metadata (diff --git, index …) parsing to 0 files; /dev/null creation hunk.

Apply to the real repo:

cd coding-hermes-tools
# apply requireHunk + boundary/EOF checks to internal/patch/patch.go
gofmt -w internal/patch/patch.go
go test ./internal/patch/ ./cmd/toolsd/ -run CHT075 -v
go test ./... && go vet ./... && make verify

The new cht075_incomplete_section_test.go and cht075_incomplete_section_cli_test.go should assert the three RED cases return a nil patch list and an errors.Is(err, patch.ErrParse) line-bearing *ParseError, and that the CLI prints no patched … and exits non-zero.


Note on verification confidence: this was validated end-to-end against a reconstruction of the parser and CLI (full source under /tmp/cht075), not the original repository, since the repo was unreachable from the sandbox. The diff above is the semantic change described by the task; line numbers/context may differ slightly in the real internal/patch/patch.go, so apply the requireHunk + boundary/EOF logic rather than assuming an exact textual patch.

Evidence & signatures

# Evidence
- Problem class: strict-unified-diff-parser-accepts-header-only-or-unpaired-file-header
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-24T00:00:08.333Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Symptom: a strict unified-diff parser accepted either an unpaired old-file header at EOF or a complete old/new header pair with no hunks. The CLI exited 0 and reported that zero or one files were patched even though seeded targets remained unchanged; a multi-file diff with a valid first section and header-only final section also reported success. Root cause: the parser tracked a pending old header but never rejected it at EOF, and it opened a FilePatch after the new header without requiring any hunk before the next section or EOF. Fix: enforce the section lifecycle in the parser, not the CLI. At a new section and at EOF, require each opened file section to contain at least one hunk; at EOF reject any still-pending old header. Return a nil partial patch list and a line-bearing typed ParseError wrapping ErrParse. Verification: separate RED commit b4990c6 proved both parser and CLI failures; GREEN commit d4b2fce made both ledger arms pass. Fresh full Go tests, make verify, all 86 ledger arms, docs checks, self-tests, board gate, independent GitReins tier-2 evaluation, live file/stdin CLI probes, and GitHub Actions all passed. Preserve controls for valid one-hunk input, multi-file input where every section has a hunk, explicit empty-hunk creation, and section-less git metadata.", "environment": "Linux; coding-hermes-tools strict patch parser and toolsd patch CLI", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "strict-unified-diff-parser-accepts-header-only-or-unpaired-file-header", "provider": "openrouter", "solved_at": "2026-09-24T00:00:08.333Z", "version": "go1.26.0"}

Answer 2

The repository itself was not present in this sandbox and could not be cloned (no GitHub credentials), so I reconstructed a faithful parser + CLI, reproduced the RED failures, applied the fix, and verified GREEN. Solution written to ~/CHT-075-solution.md:


CHT-075 — Strict unified-diff parser must refuse header-only / unpaired file sections

Repo: coding-hermes/coding-hermes-tools · Fix commit: d4b2fce · RED commit: b4990c6 Fixed files: internal/patch/patch.go, internal/patch/cht075_incomplete_section_test.go, cmd/toolsd/cht075_incomplete_section_cli_test.go

Symptom

toolsd patch exited 0 and printed patched N file(s) atomically for input that cannot patch anything:

Seeded targets were left untouched, yet the CLI reported success. It is a parser defect.

Root cause

The parser's line-oriented state machine tracks pendingOld (a --- header lacking its +++), cur (the FilePatch opened by +++), and curHunk. Section boundaries exist when the next --- arrives and at EOF, but pre-fix neither boundary enforced the lifecycle:

  1. EOF never rejected pendingOld — a dangling --- fell off the loop and Parse returned success.
  2. +++ opened a FilePatch with no hunk required — cur was appended unconditionally.
  3. On a new --- the old code set cur = nil without appending or validating, so a valid first section could be dropped while the malformed last one was reported.

The parser returned a non-nil patch with a zero/one-entry Files slice, so the CLI saw success.

Fix (internal/patch/patch.go)

        switch {
        case strings.HasPrefix(line, "--- "):
-           cur = nil
+           // Starting a new section finalises the previous one.
+           if err := requireHunk(cur, lineNo); err != nil {
+               return nil, err
+           }
+           if cur != nil {
+               p.Files = append(p.Files, *cur)
+               cur = nil
+           }
            curHunk = nil
            pendingOld = strings.TrimPrefix(line, "--- ")
...
+   // EOF is a section boundary too: reject dangling old headers and hunk-less
+   // open sections instead of emitting a partial patch.
+   if pendingOld != "" {
+       return nil, &ParseError{Line: lineNo, Err: fmt.Errorf("%w: unterminated --- header %q at end of input", ErrParse, pendingOld)}
+   }
+   if err := requireHunk(cur, lineNo); err != nil {
+       return nil, err
+   }
+   if cur != nil {
+       p.Files = append(p.Files, *cur)
+   }

+// requireHunk reports a ParseError when an open file section already exists
+// but has not accumulated at least one hunk.
+func requireHunk(cur *FilePatch, lineNo int) error {
+   if cur != nil && len(cur.Hunks) == 0 {
+       return &ParseError{
+           Line: lineNo,
+           Err:  fmt.Errorf("%w: file section %q has no hunks", ErrParse, cur.NewName),
+       }
+   }
+   return nil
+}

Key properties: section boundary and EOF both call requireHunk; EOF rejects a pending old header; every refusal returns nil, err (never a partial patch); errors are *ParseError with a Line wrapping ErrParse; the check counts hunk headers, so explicit empty-hunk creation (@@ -0,0 +0,0 @@ with no body) still parses.

Verification

RED (pre-fix):

$ go test ./internal/patch/
--- FAIL: TestRejectHeaderPairWithoutHunk ... got &{Files:[{...Hunks:[]}]}
--- FAIL: TestRejectUnpairedOldHeaderAtEOF ... got &{Files:[]}
--- FAIL: TestRejectHeaderOnlyFinalSection ... got &{Files:[{...Hunks:[]}]}

$ printf -- '--- a/a.txt\n+++ b/a.txt\n' | go run ./cmd/toolsd
patched 1 file(s) atomically        exit=0
$ printf -- '--- a/a.txt\n' | go run ./cmd/toolsd
patched 0 file(s) atomically        exit=0

GREEN (post-fix):

$ go test ./...   → ok (both packages);   $ go vet ./... → clean

$ printf -- '--- a/a.txt\n+++ b/a.txt\n' | go run ./cmd/toolsd
refusing malformed patch: patch parse error at line 2: malformed patch: file section "b/a.txt" has no hunks   exit=1
$ printf -- '--- a/a.txt\n' | go run ./cmd/toolsd
refusing malformed patch: patch parse error at line 1: malformed patch: unterminated --- header "a/a.txt" at end of input   exit=1
$ printf -- '--- a/a.txt\n+++ b/a.txt\n@@ -1 +1 @@\n-x\n+y\n' | go run ./cmd/toolsd
patched 1 file(s) atomically        exit=0
$ printf -- '...valid first...--- a/b.txt\n+++ b/b.txt\n' | go run ./cmd/toolsd
refusing malformed patch: patch parse error at line 7: malformed patch: file section "b/b.txt" has no hunks   exit=1

Controls preserved (all pass): valid one-hunk input; multi-file where every section has a hunk; explicit empty hunk; section-less git metadata (diff --git, index …) parsing to 0 files; /dev/null creation hunk.

Apply to the real repo:

cd coding-hermes-tools
# apply requireHunk + boundary/EOF checks to internal/patch/patch.go
gofmt -w internal/patch/patch.go
go test ./internal/patch/ ./cmd/toolsd/ -run CHT075 -v
go test ./... && go vet ./... && make verify

The new cht075_incomplete_section_test.go and cht075_incomplete_section_cli_test.go should assert the three RED cases return a nil patch list and an errors.Is(err, patch.ErrParse) line-bearing *ParseError, and that the CLI prints no patched … and exits non-zero.


Note on verification confidence: this was validated end-to-end against a reconstruction of the parser and CLI (full source under /tmp/cht075), not the original repository, since the repo was unreachable from the sandbox. The diff above is the semantic change described by the task; line numbers/context may differ slightly in the real internal/patch/patch.go, so apply the requireHunk + boundary/EOF logic rather than assuming an exact textual patch.

Evidence & signatures

# Evidence
- Problem class: strict-unified-diff-parser-accepts-header-only-or-unpaired-file-header
- Model: openrouter/deepseek/deepseek-v4.1-flash
- Solved: 2026-09-24T00:00:08.333Z
- Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Symptom: a strict unified-diff parser accepted either an unpaired old-file header at EOF or a complete old/new header pair with no hunks. The CLI exited 0 and reported that zero or one files were patched even though seeded targets remained unchanged; a multi-file diff with a valid first section and header-only final section also reported success. Root cause: the parser tracked a pending old header but never rejected it at EOF, and it opened a FilePatch after the new header without requiring any hunk before the next section or EOF. Fix: enforce the section lifecycle in the parser, not the CLI. At a new section and at EOF, require each opened file section to contain at least one hunk; at EOF reject any still-pending old header. Return a nil partial patch list and a line-bearing typed ParseError wrapping ErrParse. Verification: separate RED commit b4990c6 proved both parser and CLI failures; GREEN commit d4b2fce made both ledger arms pass. Fresh full Go tests, make verify, all 86 ledger arms, docs checks, self-tests, board gate, independent GitReins tier-2 evaluation, live file/stdin CLI probes, and GitHub Actions all passed. Preserve controls for valid one-hunk input, multi-file input where every section has a hunk, explicit empty-hunk creation, and section-less git metadata.", "environment": "Linux; coding-hermes-tools strict patch parser and toolsd patch CLI", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "strict-unified-diff-parser-accepts-header-only-or-unpaired-file-header", "provider": "openrouter", "solved_at": "2026-09-24T00:00:08.333Z", "version": "go1.26.0"}
Generated from the verified corpus · MIT licensedBack to the catalog