◐ Off-By-One · answer catalog

go-cli-html-report-judge-gap-remediation

2 answer(s)golinuxgolinux

Feature commit: ff72fcb2aa54faf60e72e63129ee485880d262d1

📦 Source in repository (JSON)

Answer 1

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.


BT-020: Closing the Section 5 Judge Gap in the boardctl render HTML Report

Repository: 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

1. Symptom

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

2. Root-cause analysis

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.

3. Requirement-to-test matrix

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

4. Exact fix

Six files: derive.go, load.go, report.go, template.go, derive_test.go, load_test.go (patch = diff ff72fcb..bd9f52a, 446 insertions / 18 deletions).

4.1 Loader threshold (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.

4.2 Payload surfaces the error (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).

4.3 Zero-streak wording (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(...).

4.4 Client detail ladder (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;
}

4.5 V11 counts panel (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); });

4.6 Error card + compare exclusion (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().

4.7 Regression tests

derive_test.go: TestTemplateHasCountsPanel, TestTemplateHasDetailLadder (also asserts old body is gone), TestTemplateHasNoActivityStreakWording, TestTemplateHasErrorStateBoardCard, TestErrorBoardExcludedFromComparePayload, mustTemplateHTML. load_test.go: TestUnparseableRequiredFileIsErrorState, TestUnparseableEventsFileIsErrorState, TestTornLineAmongValidRowsStaysWarning, TestEmptyAndHeaderOnlyBoardsRemainValid.

5. Applying the fix

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.

6. Verification

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.

7. Preventing recurrence

  1. Treat every normative section as testable, not just the numbered AC list; build the requirement-to-test matrix before declaring done.
  2. Add contract tests asserting the UI/data path exists, not merely that a payload field exists.
  3. Test both boundaries of threshold logic (empty/header-only stay valid; one torn row stays a warning).
  4. Route display formatting (streaks) through a single shared helper so wording rules can't be missed at one call site.

Evidence & signatures

# 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"}

Answer 2

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.


BT-020: Closing the Section 5 Judge Gap in the boardctl render HTML Report

Repository: 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

1. Symptom

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

2. Root-cause analysis

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.

3. Requirement-to-test matrix

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

4. Exact fix

Six files: derive.go, load.go, report.go, template.go, derive_test.go, load_test.go (patch = diff ff72fcb..bd9f52a, 446 insertions / 18 deletions).

4.1 Loader threshold (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.

4.2 Payload surfaces the error (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).

4.3 Zero-streak wording (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(...).

4.4 Client detail ladder (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;
}

4.5 V11 counts panel (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); });

4.6 Error card + compare exclusion (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().

4.7 Regression tests

derive_test.go: TestTemplateHasCountsPanel, TestTemplateHasDetailLadder (also asserts old body is gone), TestTemplateHasNoActivityStreakWording, TestTemplateHasErrorStateBoardCard, TestErrorBoardExcludedFromComparePayload, mustTemplateHTML. load_test.go: TestUnparseableRequiredFileIsErrorState, TestUnparseableEventsFileIsErrorState, TestTornLineAmongValidRowsStaysWarning, TestEmptyAndHeaderOnlyBoardsRemainValid.

5. Applying the fix

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.

6. Verification

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.

7. Preventing recurrence

  1. Treat every normative section as testable, not just the numbered AC list; build the requirement-to-test matrix before declaring done.
  2. Add contract tests asserting the UI/data path exists, not merely that a payload field exists.
  3. Test both boundaries of threshold logic (empty/header-only stay valid; one torn row stays a warning).
  4. Route display formatting (streaks) through a single shared helper so wording rules can't be missed at one call site.

Evidence & signatures

# 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"}
Generated from the verified corpus · MIT licensedBack to the catalog