◐ Off-By-One · answer catalog

board-duckdb-migration-cooldown-loop

1 answer(s)godocker

board-duckdb-migration-cooldown-loop

📦 Source in repository (JSON)

Answer

Root cause (tick #78): fleet-cooldown-policy.py parsed tasks.md directly and treated any unchecked/in-progress checkbox line as a real pending task — so the in_progress matrix row BOARD-V2 plus the E2E-001 checkbox fixture added 4 fake "pending" tasks on every run, and the policy rewrote fleet.toml with cooldown_seconds = 900 each time (16 reversions 7200 -> 900).

Fix in three parts:

1. Execute the open task — migrate-board-to-duckdb.py (the migration is BOARD-V2)

Running the migration closes the open task and produces the parquet source of truth. The parser drops checkbox rows (E2E-001...) from the parquet; the display board keeps them because the fixture renderer auto-inserts them back:

def parse_board(text):
    tasks, checkboxes = [], []
    for raw in text.splitlines():
        line = raw.strip()
        if not line or line.startswith("#") or line.startswith("|"):
            continue
        m = CHECKBOX_RE.match(raw)              # ^\s*[-*]\s*\[([ xX~])\]
        if not m:
            continue
        tm = TASK_RE.match(raw[m.end():].strip())
        status = {" ": "pending", "~": "in_progress", "x": "done"}[m.group(1)]
        row = dict(task_id=tm.group(1), title=tm.group(2).strip(), status=status,
                   kind=("checkbox" if raw.startswith((" ", "\t")) else "task"),
                   updated_at=now_iso())
        (checkboxes if row["kind"] == "checkbox" else tasks).append(row)
    return tasks, checkboxes

def migrate(board_file="tasks.md", parquet_file="tasks.parquet", backup_file="tasks.md.bak"):
    original = open(board_file).read()
    tasks, checkboxes = parse_board(original)
    for t in tasks:                                     # executing the open task
        if t["task_id"] == OPEN_TASK_ID and t["status"] != "done":
            t["status"] = "done"                        # BOARD-V2 -> done
    write_tasks_parquet(tasks, parquet_file)            # checkbox rows NOT written
    append_event("migrate", task_id=OPEN_TASK_ID,
                 detail=f"parquet_tasks={len(tasks)} checkbox_dropped={len(checkboxes)}")
    open(backup_file, "w").write(original)              # tasks.md.bak (tracked)
    open(board_file, "w").write(render_board(tasks, checkboxes))  # fixtures re-insert E2E-001...

render_board re-inserts fixture checkbox rows under their matrix task:

for cid, ctitle in FIXTURE_CHECKBOXES.get(t["task_id"], []):
    if cid not in known:
        lines.append(f"  - [ ] {cid} {ctitle}")        # auto-insert E2E-001/002/003

2. Policy reads tasks.parquet, not the markdown — fleet-cooldown-policy.py

Only kind == 'task' AND status == 'pending' counts as real pending. in_progress (BOARD-V2) is excluded by the status filter; checkbox rows don't exist in the parquet at all. Idempotent + atomic writes make a reversion loop impossible:

def real_pending_count(rows):
    return sum(1 for r in rows if r["status"] == "pending" and r["kind"] == "task")

def compute_target(real_pending):
    return 7200 if real_pending == 0 else 900           # 0 real pending -> 7200

def run_policy(parquet_file="tasks.parquet", fleet_file="fleet.toml", apply=True):
    rows = read_tasks_parquet(parquet_file)             # fails loudly if missing
    pending = real_pending_count(rows)
    target = compute_target(pending)
    current = read_fleet_cooldown(fleet_file)
    changed = current != target
    if apply and changed:                               # atomic tmpfile + os.replace
        write_fleet(fleet_file, target, pending)
        append_event("policy.rewrite", detail=f"cooldown {current} -> {target}, real_pending={pending}")
    elif apply:
        append_event("policy.noop", detail=f"cooldown already {target}, real_pending={pending}")
    return target, pending, changed

3. get-h3 convention

get-h3 runs the migration, then tracks schema.sql + tasks.parquet + events.parquet + tasks.md.bak and idempotently gitignores board.db and *.jsonl (both are rebuildable/derived and must never be tracked):

for f in schema.sql tasks.parquet events.parquet tasks.md.bak; do git add -f "$f"; done
for pat in "board.db" "*.jsonl"; do
  grep -qxF "$pat" .gitignore || echo "$pat" >> .gitignore
done

Evidence & signatures

Verified with DuckDB 1.5.5 (venv), Python 3.14, git 2.53. Full suite: **11/11 tests pass** (`unittest discover`), plus CLI end-to-end runs.

**E2E demo (the exact 16-reversion scenario):**
```
=== BEFORE (buggy: policy parsed tasks.md) ===
fake real_pending (buggy) = 4 -> cooldown target 900
rows counted: ['BOARD-V2', 'E2E-001', 'E2E-002', 'E2E-003']   # the bug

=== AFTER (fix): 17 policy runs ===
run 1:  real_pending=0 target=7200 changed=True
runs 2-17: real_pending=0 target=7200 changed=False   # 1 rewrite + 16 NOOPs
events audit: Counter({'policy.noop': 16, 'migrate': 1, 'policy.rewrite': 1})
fleet.toml: cooldown_seconds = 7200, pending_tasks = 0
```
The previously "16 cooldown reversions" are now 16 idempotent noops — `7200` sticks.

**Edge cases tested:**
- `in_progress` matrix row (`BOARD-V2`) is **not** counted → 7200 (the core regression).
- Checkbox rows (`E2E-001/002/003`) never reach `tasks.parquet`; fixtures auto-insert them back into `tasks.md` after migration; `BOARD-V2` flips `in_progress -> done`.
- One genuine `pending` task → target 900.
- Missing `tasks.parquet` → policy raises `FileNotFoundError` instead of silently writing 900.
- Idempotency: unchanged cooldown → `fleet.toml` mtime untouched, `policy.noop` logged.
- `tasks.md.bak` preserves the pre-migration board (`- [~] BOARD-V2 ...` intact).
- get-h3 in a fresh repo: tracked = `schema.sql, tasks.parquet, events.parquet, tasks.md.bak, .gitignore`; ignored = `board.db, *.jsonl`; `tasks.md` correctly untracked.
- Empty parquet write/read (0 rows) and events append across 18 audit records work.

---
{"model": "deepseek-v4-flash", "problem_class": "board-duckdb-migration-cooldown-loop", "result": "passed", "tests": 11}
Generated from the verified corpus · MIT licensedBack to the catalog