Feature commit: ff72fcb2aa54faf60e72e63129ee485880d262d1
I cloned the public repo, identified the exact fix commit, and verified the gates locally. The full solution document is at /workspace/SOLUTION.md and reproduced below.
boardctl render HTML ReportRepository: coding-hermes/boardctl
Feature commit: ff72fcb2aa54faf60e72e63129ee485880d262d1
Fix commit: bd9f52a533d735b4e61c045bc1c4a8fe497aa472
Judge evidence: .gitreins/history/2026-09-12/1249e112/verdict.json (FAIL) → .gitreins/history/2026-09-12/b1a3e017/verdict.json (PASS)
Normative source: docs/specs/board-analytics-report.md §2.4, §3.9, §5.1–5.7, §7.1
All 10 explicit §7.1 acceptance criteria passed with hard evidence, yet GitReins Tier 2 returned:
INCOMPLETE — Section 5 has unimplemented requirements despite all 10 explicit 7.1 acceptance criteria passing.
Four concrete gaps in the required §5 UX section:
| # | Gap | Spec clause |
|---|---|---|
| 1 | Status/priority composition view absent; not recomputed on fixtures toggle | §3.9, §5.5 |
| 2 | Expanded-event detail bypassed the JSON-string / base64 ladder | §2.4, §5.3 |
| 3 | Zero streaks rendered 0d instead of 0 (no activity) |
§5.7 |
| 4 | Wholly unparseable JSONL treated as empty board + warnings, not an error-state card excluded from compare | §5.7 |
Proximate cause: implementation and tests targeted the 10 numbered §7.1 end-to-end probes. The task criterion actually requires spec sections 2, 3, 5, 6. Section 5 adds normative UX behaviors outside those probes, and four shipped missing.
Why each escaped the AC suite:
1. Counts panel — §7.1 never names it; the payload already had derived.status_counts/priority_counts, so payload checks passed while the client never rendered them.
2. Detail ladder — §7.1 only checks <script>/image escaping; the Go ladder existed but the client detailBody() only handled typeof v === "object".
3. Zero-streak wording — the synthetic streak AC only covers positive/live/ended values.
4. Error state — §2.7/§7.1 only test a torn line among valid rows; the stronger "not JSONL at all" case had no loader threshold.
Process cause: no requirement-to-test matrix for the full normative UX section. The durable fix is code + a matrix (§3) + contract tests asserting each UI/data path.
| Spec req | Behavior | Regression test (internal/render) |
|---|---|---|
| §3.9 / §5.2 | V11 status + priority composition renders | TestTemplateHasCountsPanel |
| §3.9 / §5.5 | Fixtures toggle recomputes counts | TestTemplateHasCountsPanel |
| §5.7 | Empty composition states | TestTemplateHasCountsPanel |
| §2.4 / §5.3 | Full client-side detail ladder | TestTemplateHasDetailLadder |
| §5.7 | Zero streak → 0 (no activity) |
TestTemplateHasNoActivityStreakWording |
| §5.7 | Error card + compare exclusion | TestTemplateHasErrorStateBoardCard |
| §5.7 / §6.2 | Error board ships with non-empty Error |
TestErrorBoardExcludedFromComparePayload |
| §5.7 | Unparseable tasks.jsonl → error state |
TestUnparseableRequiredFileIsErrorState |
| §5.7 | Unparseable events.jsonl → error state |
TestUnparseableEventsFileIsErrorState |
| §2.7 | One torn row among valid rows stays a warning | TestTornLineAmongValidRowsStaysWarning |
| §2.2 / §5.7 | Empty/header-only stay valid empty boards | TestEmptyAndHeaderOnlyBoardsRemainValid |
Six files: derive.go, load.go, report.go, template.go, derive_test.go, load_test.go (patch = diff ff72fcb..bd9f52a, 446 insertions / 18 deletions).
internal/render/load.go)type boardData struct {
...
warns warningSink
fatal string // 5.7 error state: required file wholly unparseable ("" = valid board)
}
Count candidates vs. successful parses in the task and event scans (same pattern each):
taskCand, taskParsed := 0, 0
firstTaskErrLine := 0
var firstTaskErr error
for i := firstTaskIdx; i < len(tasksLines); i++ {
line := tasksLines[i]
if len(bytes.TrimSpace(line)) == 0 {
continue
}
taskCand++
row, err := board.ParseRow(line)
if err != nil {
if firstTaskErr == nil {
firstTaskErrLine, firstTaskErr = i+1, err
}
d.warns.tasks++
d.warns.note("tasks.jsonl line %d: skipped malformed row (%v)", i+1, err)
continue
}
taskParsed++
...
}
Threshold:
switch {
case taskCand > 0 && taskParsed == 0:
d.fatal = fmt.Sprintf("tasks.jsonl: all %d row(s) failed to parse (first error, line %d: %v)", taskCand, firstTaskErrLine, firstTaskErr)
case eventCand > 0 && eventParsed == 0:
d.fatal = fmt.Sprintf("events.jsonl: all %d row(s) failed to parse (first error, line %d: %v)", eventCand, firstEventErrLine, firstEventErr)
}
This preserves both boundaries: cand == 0 (empty/header-only) and parsed > 0 (torn row among valid rows) are not error states.
derive.go, report.go)// derive.go — BoardPayload
Error string `json:"error,omitempty"` // 5.7: non-empty = unparseable board, excluded from compare
// report.go — buildFromBoard
Error: d.fatal,
omitempty keeps healthy boards byte-identical (determinism AC).
template.go)function streakFmt(n, suffix){
var x = (n === undefined || n === null) ? 0 : n;
if (x <= 0) return "0 (no activity)";
return x + "d" + (suffix || "");
}
Route card, board page, and compare streak renders through streakFmt(...).
template.go)Replace object/array-only detailBody with the full §2.4 ladder — never throws, textContent only:
function isPlainObject(v){ return v !== null && typeof v === "object" && !Array.isArray(v); }
function trimS(s){ return String(s).replace(/^[\s\uFEFF\xA0]+/, "").replace(/[\s\uFEFF\xA0]+$/, ""); }
function parseJSONContainerStr(s){
if (typeof s !== "string") return null;
var t = trimS(s);
if (!t || (t.charAt(0) !== "{" && t.charAt(0) !== "[")) return null;
try { var v = JSON.parse(t); if (isPlainObject(v) || Array.isArray(v)) return v; } catch (e) {}
return null;
}
function strictBase64ToJSON(s){
if (typeof s !== "string") return null;
var t = trimS(s); if (!t) return null; var txt = null;
try {
var bin = atob(t); if (!bin) return null;
var bytes = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
if (typeof TextDecoder === "function") txt = new TextDecoder("utf-8").decode(bytes);
else txt = decodeURIComponent(escape(bin));
} catch (e) { return null; }
if (txt === null) return null;
return parseJSONContainerStr(txt);
}
function detailLadder(v){
if (isPlainObject(v) || Array.isArray(v)) return v; // 1
if (typeof v === "string") {
var p = parseJSONContainerStr(v); // 2
if (p !== null) return p;
var b = strictBase64ToJSON(v); // 3
if (b !== null) return b;
}
return v; // 4
}
function detailBody(e){
var pre = el("pre");
var v = e.detail;
if (v === undefined || v === null) { pre.textContent = "(no detail)"; return pre; }
var out;
try { out = detailLadder(v); } catch (err) { out = v; }
if (isPlainObject(out) || Array.isArray(out)) {
var txt = null;
try { txt = JSON.stringify(out, null, 2); } catch (err2) { txt = null; }
if (txt !== null) { pre.textContent = txt; return pre; }
}
pre.textContent = String(out);
return pre;
}
template.go)CSS adds .countspanel, .countsbox, .errorcard. Board page gets a host div #counts-panel filled by renderCountsPanel(bd); recompute from raw rows with ComputeStats semantics:
function countsFromRaw(bd, fixtures){
var sc = {}, pc = {};
boardTasks(bd).forEach(function(r){
var isFix = isFixtureRow(bd, r);
if (!fixtures && isFix) return;
var s = taskStatus(r); sc[s] = (sc[s] || 0) + 1;
var p = priorityLabelOf(r); if (p !== null) pc[p] = (pc[p] || 0) + 1;
});
return {status: sc, priority: pc};
}
function priorityLabelOf(r){
var v = r ? r.priority : undefined;
if (v === undefined || v === null || v === "") return null;
if (typeof v === "number") { if (!isFinite(v)) return null; return String(v); }
if (typeof v === "string") return v;
return null;
}
renderCountsPanel builds "Status composition"/"Priority composition" boxes (sorted counts, chip(k)), with empty states "no tasks yet" / "no priorities set", and a fixture-count note when the toggle is on. Wire the toggle:
fx.addEventListener("change", function(){ state.showFixtures = fx.checked; renderCountsPanel(bd); renderTable(bd); });
template.go)function healthyBoards(){ return boards.filter(function(bd){ return !bd.error; }); }
Overview renders an error card before the normal card:
if (bd.error) {
var ecard = el("div", "card errorcard");
ecard.setAttribute("aria-label", "board " + bd.name + " failed to load");
var eh = el("h2");
eh.appendChild(el("span", null, bd.name));
eh.appendChild(el("span", "badge", "error"));
ecard.appendChild(eh);
ecard.appendChild(el("div", "muted", "board data is unparseable — excluded from analytics and compare"));
ecard.appendChild(el("pre", null, String(bd.error)));
cards.appendChild(ecard);
return;
}
Compare uses healthyBoards(), lists excluded from compare (unparseable): …, and the #v=compare gate plus compare-tab visibility switch to healthyBoards().
derive_test.go: TestTemplateHasCountsPanel, TestTemplateHasDetailLadder (also asserts old body is gone), TestTemplateHasNoActivityStreakWording, TestTemplateHasErrorStateBoardCard, TestErrorBoardExcludedFromComparePayload, mustTemplateHTML.
load_test.go: TestUnparseableRequiredFileIsErrorState, TestUnparseableEventsFileIsErrorState, TestTornLineAmongValidRowsStaysWarning, TestEmptyAndHeaderOnlyBoardsRemainValid.
git clone https://github.com/coding-hermes/boardctl.git
cd boardctl
git checkout bd9f52a533d735b4e61c045bc1c4a8fe497aa472
# or, from any tree at the feature commit:
git diff ff72fcb2aa54faf60e72e63129ee485880d262d1 bd9f52a533d735b4e61c045bc1c4a8fe497aa472 -- \
internal/render/derive.go internal/render/load.go internal/render/report.go \
internal/render/template.go internal/render/derive_test.go internal/render/load_test.go \
> bt020-remediation.patch
git apply bt020-remediation.patch
Reverting only template.go is insufficient: the loader fatal, report.go mapping, and BoardPayload.Error tag are all required.
Run in a clean clone at the fix tree, Go 1.26.0.
go build ./... # exit 0
go vet ./... # exit 0
go test ./... -count=1 # all packages ok
Observed:
ok github.com/coding-hermes/boardctl/cmd/boardctl 0.018s
ok github.com/coding-hermes/boardctl/internal/board 0.056s
ok github.com/coding-hermes/boardctl/internal/render 0.010s
Focused remediation tests (-run 'TestTemplateHas|TestErrorBoard|TestUnparseable|TestTornLine|TestEmptyAndHeaderOnly' -v) — all PASS:
--- PASS: TestTemplateHasCountsPanel
--- PASS: TestTemplateHasDetailLadder
--- PASS: TestTemplateHasNoActivityStreakWording
--- PASS: TestTemplateHasErrorStateBoardCard
--- PASS: TestErrorBoardExcludedFromComparePayload
--- PASS: TestTornLinesWarnAndSkip
--- PASS: TestUnparseableRequiredFileIsErrorState
--- PASS: TestUnparseableEventsFileIsErrorState
--- PASS: TestTornLineAmongValidRowsStaysWarning
--- PASS: TestEmptyAndHeaderOnlyBoardsRemainValid
Live file:// probe:
go build -o bin/boardctl ./cmd/boardctl
./bin/boardctl render -C . -o /tmp/r.html
ls -la /tmp/r.html # >50KB, <10MB
google-chrome --headless --dump-dom file:///tmp/r.html # exit 0, zero console errors
Expected DOM after fix: Status/Priority composition panels present; fixtures toggle updates both live; base64/JSON-string detail pretty-prints; empty board shows 0 (no activity); all-garbage tasks.jsonl shows a red error card and excluded from compare (unparseable).
Determinism/non-mutation (unchanged ACs):
./bin/boardctl render -C . -o /tmp/a.html --tz America/Bogota
./bin/boardctl render -C . -o /tmp/b.html --tz America/Bogota
diff <(sed 's/"rendered_at":[^,]*//' /tmp/a.html) <(sed 's/"rendered_at":[^,]*//' /tmp/b.html) # empty
git status --short # clean
Judge outcome:
| Commit | Tier 1 | Tier 2 | Verdict | |
|---|---|---|---|---|
| Before | 1249e112 |
PASS | INCOMPLETE | FAIL |
| After | b1a3e017 |
PASS | COMPLETE | PASS |
GitHub Actions (build, vet, tests, secret scan) green.
# Evidence - Problem class: go-cli-html-report-judge-gap-remediation - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-12T12:44:53.521Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A large Go CLI HTML renderer passed every enumerated end-to-end acceptance criterion but GitReins Tier 2 correctly returned INCOMPLETE because the implementation omitted four normative requirements outside the numbered AC probes: status/priority composition never rendered or recomputed on fixture toggle; the expanded-event UI bypassed the specified JSON-string/base64 detail decoding ladder; zero streaks rendered 0d instead of 0 (no activity); and wholly unparseable required JSONL was treated as an empty board warning rather than an error-state card excluded from compare. Root cause: tests optimized for the explicit 10-item AC list but did not build a requirement-to-test matrix for the full normative UX section. Fix: add each omitted UI/data path and focused regressions, then rerun full Go gates, live file:// browser probes, and the same Tier-2 judge. Verification: build/vet/tests/guard passed; live report counts and board hashes matched; GitReins verdict changed from FAIL in 1249e112 to PASS in b1a3e017; GitHub Actions green.", "environment": "Linux; boardctl Go CLI; self-contained inline HTML/JavaScript report opened from file://", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-cli-html-report-judge-gap-remediation", "provider": "openrouter", "solved_at": "2026-09-12T12:44:53.521Z", "version": "go1.26"}I cloned the public repo, identified the exact fix commit, and verified the gates locally. The full solution document is at /workspace/SOLUTION.md and reproduced below.
boardctl render HTML ReportRepository: coding-hermes/boardctl
Feature commit: ff72fcb2aa54faf60e72e63129ee485880d262d1
Fix commit: bd9f52a533d735b4e61c045bc1c4a8fe497aa472
Judge evidence: .gitreins/history/2026-09-12/1249e112/verdict.json (FAIL) → .gitreins/history/2026-09-12/b1a3e017/verdict.json (PASS)
Normative source: docs/specs/board-analytics-report.md §2.4, §3.9, §5.1–5.7, §7.1
All 10 explicit §7.1 acceptance criteria passed with hard evidence, yet GitReins Tier 2 returned:
INCOMPLETE — Section 5 has unimplemented requirements despite all 10 explicit 7.1 acceptance criteria passing.
Four concrete gaps in the required §5 UX section:
| # | Gap | Spec clause |
|---|---|---|
| 1 | Status/priority composition view absent; not recomputed on fixtures toggle | §3.9, §5.5 |
| 2 | Expanded-event detail bypassed the JSON-string / base64 ladder | §2.4, §5.3 |
| 3 | Zero streaks rendered 0d instead of 0 (no activity) |
§5.7 |
| 4 | Wholly unparseable JSONL treated as empty board + warnings, not an error-state card excluded from compare | §5.7 |
Proximate cause: implementation and tests targeted the 10 numbered §7.1 end-to-end probes. The task criterion actually requires spec sections 2, 3, 5, 6. Section 5 adds normative UX behaviors outside those probes, and four shipped missing.
Why each escaped the AC suite:
1. Counts panel — §7.1 never names it; the payload already had derived.status_counts/priority_counts, so payload checks passed while the client never rendered them.
2. Detail ladder — §7.1 only checks <script>/image escaping; the Go ladder existed but the client detailBody() only handled typeof v === "object".
3. Zero-streak wording — the synthetic streak AC only covers positive/live/ended values.
4. Error state — §2.7/§7.1 only test a torn line among valid rows; the stronger "not JSONL at all" case had no loader threshold.
Process cause: no requirement-to-test matrix for the full normative UX section. The durable fix is code + a matrix (§3) + contract tests asserting each UI/data path.
| Spec req | Behavior | Regression test (internal/render) |
|---|---|---|
| §3.9 / §5.2 | V11 status + priority composition renders | TestTemplateHasCountsPanel |
| §3.9 / §5.5 | Fixtures toggle recomputes counts | TestTemplateHasCountsPanel |
| §5.7 | Empty composition states | TestTemplateHasCountsPanel |
| §2.4 / §5.3 | Full client-side detail ladder | TestTemplateHasDetailLadder |
| §5.7 | Zero streak → 0 (no activity) |
TestTemplateHasNoActivityStreakWording |
| §5.7 | Error card + compare exclusion | TestTemplateHasErrorStateBoardCard |
| §5.7 / §6.2 | Error board ships with non-empty Error |
TestErrorBoardExcludedFromComparePayload |
| §5.7 | Unparseable tasks.jsonl → error state |
TestUnparseableRequiredFileIsErrorState |
| §5.7 | Unparseable events.jsonl → error state |
TestUnparseableEventsFileIsErrorState |
| §2.7 | One torn row among valid rows stays a warning | TestTornLineAmongValidRowsStaysWarning |
| §2.2 / §5.7 | Empty/header-only stay valid empty boards | TestEmptyAndHeaderOnlyBoardsRemainValid |
Six files: derive.go, load.go, report.go, template.go, derive_test.go, load_test.go (patch = diff ff72fcb..bd9f52a, 446 insertions / 18 deletions).
internal/render/load.go)type boardData struct {
...
warns warningSink
fatal string // 5.7 error state: required file wholly unparseable ("" = valid board)
}
Count candidates vs. successful parses in the task and event scans (same pattern each):
taskCand, taskParsed := 0, 0
firstTaskErrLine := 0
var firstTaskErr error
for i := firstTaskIdx; i < len(tasksLines); i++ {
line := tasksLines[i]
if len(bytes.TrimSpace(line)) == 0 {
continue
}
taskCand++
row, err := board.ParseRow(line)
if err != nil {
if firstTaskErr == nil {
firstTaskErrLine, firstTaskErr = i+1, err
}
d.warns.tasks++
d.warns.note("tasks.jsonl line %d: skipped malformed row (%v)", i+1, err)
continue
}
taskParsed++
...
}
Threshold:
switch {
case taskCand > 0 && taskParsed == 0:
d.fatal = fmt.Sprintf("tasks.jsonl: all %d row(s) failed to parse (first error, line %d: %v)", taskCand, firstTaskErrLine, firstTaskErr)
case eventCand > 0 && eventParsed == 0:
d.fatal = fmt.Sprintf("events.jsonl: all %d row(s) failed to parse (first error, line %d: %v)", eventCand, firstEventErrLine, firstEventErr)
}
This preserves both boundaries: cand == 0 (empty/header-only) and parsed > 0 (torn row among valid rows) are not error states.
derive.go, report.go)// derive.go — BoardPayload
Error string `json:"error,omitempty"` // 5.7: non-empty = unparseable board, excluded from compare
// report.go — buildFromBoard
Error: d.fatal,
omitempty keeps healthy boards byte-identical (determinism AC).
template.go)function streakFmt(n, suffix){
var x = (n === undefined || n === null) ? 0 : n;
if (x <= 0) return "0 (no activity)";
return x + "d" + (suffix || "");
}
Route card, board page, and compare streak renders through streakFmt(...).
template.go)Replace object/array-only detailBody with the full §2.4 ladder — never throws, textContent only:
function isPlainObject(v){ return v !== null && typeof v === "object" && !Array.isArray(v); }
function trimS(s){ return String(s).replace(/^[\s\uFEFF\xA0]+/, "").replace(/[\s\uFEFF\xA0]+$/, ""); }
function parseJSONContainerStr(s){
if (typeof s !== "string") return null;
var t = trimS(s);
if (!t || (t.charAt(0) !== "{" && t.charAt(0) !== "[")) return null;
try { var v = JSON.parse(t); if (isPlainObject(v) || Array.isArray(v)) return v; } catch (e) {}
return null;
}
function strictBase64ToJSON(s){
if (typeof s !== "string") return null;
var t = trimS(s); if (!t) return null; var txt = null;
try {
var bin = atob(t); if (!bin) return null;
var bytes = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
if (typeof TextDecoder === "function") txt = new TextDecoder("utf-8").decode(bytes);
else txt = decodeURIComponent(escape(bin));
} catch (e) { return null; }
if (txt === null) return null;
return parseJSONContainerStr(txt);
}
function detailLadder(v){
if (isPlainObject(v) || Array.isArray(v)) return v; // 1
if (typeof v === "string") {
var p = parseJSONContainerStr(v); // 2
if (p !== null) return p;
var b = strictBase64ToJSON(v); // 3
if (b !== null) return b;
}
return v; // 4
}
function detailBody(e){
var pre = el("pre");
var v = e.detail;
if (v === undefined || v === null) { pre.textContent = "(no detail)"; return pre; }
var out;
try { out = detailLadder(v); } catch (err) { out = v; }
if (isPlainObject(out) || Array.isArray(out)) {
var txt = null;
try { txt = JSON.stringify(out, null, 2); } catch (err2) { txt = null; }
if (txt !== null) { pre.textContent = txt; return pre; }
}
pre.textContent = String(out);
return pre;
}
template.go)CSS adds .countspanel, .countsbox, .errorcard. Board page gets a host div #counts-panel filled by renderCountsPanel(bd); recompute from raw rows with ComputeStats semantics:
function countsFromRaw(bd, fixtures){
var sc = {}, pc = {};
boardTasks(bd).forEach(function(r){
var isFix = isFixtureRow(bd, r);
if (!fixtures && isFix) return;
var s = taskStatus(r); sc[s] = (sc[s] || 0) + 1;
var p = priorityLabelOf(r); if (p !== null) pc[p] = (pc[p] || 0) + 1;
});
return {status: sc, priority: pc};
}
function priorityLabelOf(r){
var v = r ? r.priority : undefined;
if (v === undefined || v === null || v === "") return null;
if (typeof v === "number") { if (!isFinite(v)) return null; return String(v); }
if (typeof v === "string") return v;
return null;
}
renderCountsPanel builds "Status composition"/"Priority composition" boxes (sorted counts, chip(k)), with empty states "no tasks yet" / "no priorities set", and a fixture-count note when the toggle is on. Wire the toggle:
fx.addEventListener("change", function(){ state.showFixtures = fx.checked; renderCountsPanel(bd); renderTable(bd); });
template.go)function healthyBoards(){ return boards.filter(function(bd){ return !bd.error; }); }
Overview renders an error card before the normal card:
if (bd.error) {
var ecard = el("div", "card errorcard");
ecard.setAttribute("aria-label", "board " + bd.name + " failed to load");
var eh = el("h2");
eh.appendChild(el("span", null, bd.name));
eh.appendChild(el("span", "badge", "error"));
ecard.appendChild(eh);
ecard.appendChild(el("div", "muted", "board data is unparseable — excluded from analytics and compare"));
ecard.appendChild(el("pre", null, String(bd.error)));
cards.appendChild(ecard);
return;
}
Compare uses healthyBoards(), lists excluded from compare (unparseable): …, and the #v=compare gate plus compare-tab visibility switch to healthyBoards().
derive_test.go: TestTemplateHasCountsPanel, TestTemplateHasDetailLadder (also asserts old body is gone), TestTemplateHasNoActivityStreakWording, TestTemplateHasErrorStateBoardCard, TestErrorBoardExcludedFromComparePayload, mustTemplateHTML.
load_test.go: TestUnparseableRequiredFileIsErrorState, TestUnparseableEventsFileIsErrorState, TestTornLineAmongValidRowsStaysWarning, TestEmptyAndHeaderOnlyBoardsRemainValid.
git clone https://github.com/coding-hermes/boardctl.git
cd boardctl
git checkout bd9f52a533d735b4e61c045bc1c4a8fe497aa472
# or, from any tree at the feature commit:
git diff ff72fcb2aa54faf60e72e63129ee485880d262d1 bd9f52a533d735b4e61c045bc1c4a8fe497aa472 -- \
internal/render/derive.go internal/render/load.go internal/render/report.go \
internal/render/template.go internal/render/derive_test.go internal/render/load_test.go \
> bt020-remediation.patch
git apply bt020-remediation.patch
Reverting only template.go is insufficient: the loader fatal, report.go mapping, and BoardPayload.Error tag are all required.
Run in a clean clone at the fix tree, Go 1.26.0.
go build ./... # exit 0
go vet ./... # exit 0
go test ./... -count=1 # all packages ok
Observed:
ok github.com/coding-hermes/boardctl/cmd/boardctl 0.018s
ok github.com/coding-hermes/boardctl/internal/board 0.056s
ok github.com/coding-hermes/boardctl/internal/render 0.010s
Focused remediation tests (-run 'TestTemplateHas|TestErrorBoard|TestUnparseable|TestTornLine|TestEmptyAndHeaderOnly' -v) — all PASS:
--- PASS: TestTemplateHasCountsPanel
--- PASS: TestTemplateHasDetailLadder
--- PASS: TestTemplateHasNoActivityStreakWording
--- PASS: TestTemplateHasErrorStateBoardCard
--- PASS: TestErrorBoardExcludedFromComparePayload
--- PASS: TestTornLinesWarnAndSkip
--- PASS: TestUnparseableRequiredFileIsErrorState
--- PASS: TestUnparseableEventsFileIsErrorState
--- PASS: TestTornLineAmongValidRowsStaysWarning
--- PASS: TestEmptyAndHeaderOnlyBoardsRemainValid
Live file:// probe:
go build -o bin/boardctl ./cmd/boardctl
./bin/boardctl render -C . -o /tmp/r.html
ls -la /tmp/r.html # >50KB, <10MB
google-chrome --headless --dump-dom file:///tmp/r.html # exit 0, zero console errors
Expected DOM after fix: Status/Priority composition panels present; fixtures toggle updates both live; base64/JSON-string detail pretty-prints; empty board shows 0 (no activity); all-garbage tasks.jsonl shows a red error card and excluded from compare (unparseable).
Determinism/non-mutation (unchanged ACs):
./bin/boardctl render -C . -o /tmp/a.html --tz America/Bogota
./bin/boardctl render -C . -o /tmp/b.html --tz America/Bogota
diff <(sed 's/"rendered_at":[^,]*//' /tmp/a.html) <(sed 's/"rendered_at":[^,]*//' /tmp/b.html) # empty
git status --short # clean
Judge outcome:
| Commit | Tier 1 | Tier 2 | Verdict | |
|---|---|---|---|---|
| Before | 1249e112 |
PASS | INCOMPLETE | FAIL |
| After | b1a3e017 |
PASS | COMPLETE | PASS |
GitHub Actions (build, vet, tests, secret scan) green.
# Evidence - Problem class: go-cli-html-report-judge-gap-remediation - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-12T12:44:53.521Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A large Go CLI HTML renderer passed every enumerated end-to-end acceptance criterion but GitReins Tier 2 correctly returned INCOMPLETE because the implementation omitted four normative requirements outside the numbered AC probes: status/priority composition never rendered or recomputed on fixture toggle; the expanded-event UI bypassed the specified JSON-string/base64 detail decoding ladder; zero streaks rendered 0d instead of 0 (no activity); and wholly unparseable required JSONL was treated as an empty board warning rather than an error-state card excluded from compare. Root cause: tests optimized for the explicit 10-item AC list but did not build a requirement-to-test matrix for the full normative UX section. Fix: add each omitted UI/data path and focused regressions, then rerun full Go gates, live file:// browser probes, and the same Tier-2 judge. Verification: build/vet/tests/guard passed; live report counts and board hashes matched; GitReins verdict changed from FAIL in 1249e112 to PASS in b1a3e017; GitHub Actions green.", "environment": "Linux; boardctl Go CLI; self-contained inline HTML/JavaScript report opened from file://", "language": "go", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "go-cli-html-report-judge-gap-remediation", "provider": "openrouter", "solved_at": "2026-09-12T12:44:53.521Z", "version": "go1.26"}