TASKIDRE = re.compile(r"[A-Za-z]A-Za-z0-9-\d+[A-Za-z]")
The silent drops came from three parser defects in migrate-board-to-duckdb.py. The fix (full file: ~/board-migration/migrate-board-to-duckdb.py) makes every bullet a first-class row, drives completion from the status column, then verifies with a diff, backfills, and audits.
1. Lenient task-ID extraction (fixes BUG-E2E-001a-style drops)
# OLD (buggy): TASK_ID_RE = re.compile(r"^([A-Z]+-\d+)") # rejects E2E, 001a
TASK_ID_RE = re.compile(r"[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*-\d+[A-Za-z]*")
FALLBACK_ID_RE = re.compile(r"[A-Za-z0-9]+(?:-[A-Za-z0-9]+)+") # last resort
def extract_id(body: str) -> Optional[str]:
m = TASK_ID_RE.match(body)
if m: return m.group(0)
m = FALLBACK_ID_RE.match(body)
return m.group(0) if m else None
# ID-less rows are never dropped — they get a generated placeholder
if task_id is None:
task_id = f"UNPARSED-{slugify(section)}-{counter:03d}" # + generated=True
2. Parse every ## section, not just Active* (fixes ## Dogfood Findings)
if stripped.startswith("## "):
section = stripped[3:].strip() # any section; name stored on the row
continue
if section and is_row_bullet(line):
# capture bullet + its sub-lines ("- status: …", nested bullets) into a block
is_row_bullet requires a checkbox or a leading machine ID, so - status: fields and - sub detail bullets are never mistaken for rows:
def is_row_bullet(line: str) -> bool:
if not BULLET_RE.match(line) or FIELD_RE.match(line): return False
body = BULLET_RE.sub("", line).strip()
if CHECKBOX_RE.match(body): return True
if len(line) - len(line.lstrip()) > 2: return False
return extract_id(re.sub(r"^[^\w-]+", "", body)) is not None
3. Completion comes from the status: column; ✅ is only a signal (fixes silent "complete")
COMPLETE_STATUSES = {"done","closed","complete","resolved","shipped","fixed","verified"}
@dataclass
class Row:
...
@property
def complete(self) -> bool: # truth = board's status column
if not self.status: return False
head = self.status.strip().rstrip(".!").lower().split()[0]
return head in COMPLETE_STATUSES
@property
def mismatch(self) -> bool: # ✅/checkbox disagrees -> eyeball it
return (self.checked or self.emoji_done) != self.complete
4. Post-migration verification: diff → backfill → audit
# diff: DB task counts vs manual per-section bullet count straight from the board
def diff_report(con, board_text):
manual = manual_section_counts(board_text) # human-style bullet count
db = dict(con.execute("SELECT section, COUNT(DISTINCT task_id) FROM tasks GROUP BY section"))
... # prints OK/MISMATCH per section, Active* sections highlighted
# backfill: re-insert any board row whose ID is missing, with an audit-trail entry
def backfill_missing(con, rows):
existing = {r[0] for r in con.execute("SELECT task_id FROM tasks")}
missing = [r for r in rows if r.task_id not in existing]
... # INSERT ... ON CONFLICT (task_id) DO UPDATE, backfilled=TRUE
# + INSERT INTO migration_audit (task_id, reason, source_text)
# audit: eyeball every non-complete / mismatch / generated row with raw source text
def audit_report(rows):
for r in [r for r in rows if not r.complete or r.mismatch or r.generated]:
print(f" [{r.section}] {r.task_id} ({'; '.join(reasons)})")
for src in r.raw.splitlines(): print(f" {src}")
migrate() also runs ALTER TABLE tasks ADD COLUMN … for any missing columns, so an existing DB produced by the old script is upgraded in place instead of erroring.
**Reproduced the old drops first** (test `test_old_regex_would_have_dropped_them`): on the sample board, the old parser (`^([A-Z]+-\d+)` + Active-only sections) kept exactly `[TASK-0012, TASK-0044, TASK-0055, BUG-2002]` — 5 of 9 rows silently dropped, including all of `## Dogfood Findings`. **After the fix** — CLI run on `board.md`: ``` DIFF (Tasks found vs manual row count) OK Active Sprint [Active*]: found=5 manual=5 OK Dogfood Findings: found=3 manual=3 OK Backlog: found=1 manual=1 => all sections in sync ``` DB contents: all **9/9 rows** present; `BUG-E2E-001a` and `BUG-E2E-002b` migrated; `TASK-0044` (✅ in title, `status: In Progress`) is `complete=False` with `mismatch=True`; `TASK-0055` (`[x]` but no status) is flagged for eyeball; the ID-less row became `UNPARSED-Dogfood-Findings-001`. **Backfill recovery demo** (DB that already lost rows to the old parser, old 2-column schema): migrate upgraded the schema, upserted all 9; after a simulated re-drop of `TASK-0099`, `backfill_missing` restored it and wrote an `migration_audit` row — total back to 9, diff clean. **Edge cases covered by the 22 passing pytest tests** (`~/board-migration/test_migration.py`): - lowercase + alpha-suffix IDs (`BUG-E2E-001a`, `BUG-E2E-002b`, `TASK-001a`) - ✅ before the ID (`✅ BUG-E2E-009c`) and after the ID - non-Active sections parsed with section names stored; empty sections count 0 - ✅ in title vs status column (not marked complete; mismatch flagged) - `[x]` without status → audit; genuinely Done rows not flagged - ID-less rows get placeholders, never dropped - duplicate IDs across sections counted distinctly - CRLF endings + trailing whitespace; nested sub-bullets not mistaken for rows - idempotency: migrate/backfill run twice → no duplicates (upsert)
{"model": "deepseek-v4-flash", "problem_class": "board-duckdb-migration-parser-drop", "result": "passed", "tests": 22}The silent drops came from three parser defects in migrate-board-to-duckdb.py. The fix (full file: ~/board-migration/migrate-board-to-duckdb.py) makes every bullet a first-class row, drives completion from the status column, then verifies with a diff, backfills, and audits.
1. Lenient task-ID extraction (fixes BUG-E2E-001a-style drops)
# OLD (buggy): TASK_ID_RE = re.compile(r"^([A-Z]+-\d+)") # rejects E2E, 001a
TASK_ID_RE = re.compile(r"[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*-\d+[A-Za-z]*")
FALLBACK_ID_RE = re.compile(r"[A-Za-z0-9]+(?:-[A-Za-z0-9]+)+") # last resort
def extract_id(body: str) -> Optional[str]:
m = TASK_ID_RE.match(body)
if m: return m.group(0)
m = FALLBACK_ID_RE.match(body)
return m.group(0) if m else None
# ID-less rows are never dropped — they get a generated placeholder
if task_id is None:
task_id = f"UNPARSED-{slugify(section)}-{counter:03d}" # + generated=True
2. Parse every ## section, not just Active* (fixes ## Dogfood Findings)
if stripped.startswith("## "):
section = stripped[3:].strip() # any section; name stored on the row
continue
if section and is_row_bullet(line):
# capture bullet + its sub-lines ("- status: …", nested bullets) into a block
is_row_bullet requires a checkbox or a leading machine ID, so - status: fields and - sub detail bullets are never mistaken for rows:
def is_row_bullet(line: str) -> bool:
if not BULLET_RE.match(line) or FIELD_RE.match(line): return False
body = BULLET_RE.sub("", line).strip()
if CHECKBOX_RE.match(body): return True
if len(line) - len(line.lstrip()) > 2: return False
return extract_id(re.sub(r"^[^\w-]+", "", body)) is not None
3. Completion comes from the status: column; ✅ is only a signal (fixes silent "complete")
COMPLETE_STATUSES = {"done","closed","complete","resolved","shipped","fixed","verified"}
@dataclass
class Row:
...
@property
def complete(self) -> bool: # truth = board's status column
if not self.status: return False
head = self.status.strip().rstrip(".!").lower().split()[0]
return head in COMPLETE_STATUSES
@property
def mismatch(self) -> bool: # ✅/checkbox disagrees -> eyeball it
return (self.checked or self.emoji_done) != self.complete
4. Post-migration verification: diff → backfill → audit
# diff: DB task counts vs manual per-section bullet count straight from the board
def diff_report(con, board_text):
manual = manual_section_counts(board_text) # human-style bullet count
db = dict(con.execute("SELECT section, COUNT(DISTINCT task_id) FROM tasks GROUP BY section"))
... # prints OK/MISMATCH per section, Active* sections highlighted
# backfill: re-insert any board row whose ID is missing, with an audit-trail entry
def backfill_missing(con, rows):
existing = {r[0] for r in con.execute("SELECT task_id FROM tasks")}
missing = [r for r in rows if r.task_id not in existing]
... # INSERT ... ON CONFLICT (task_id) DO UPDATE, backfilled=TRUE
# + INSERT INTO migration_audit (task_id, reason, source_text)
# audit: eyeball every non-complete / mismatch / generated row with raw source text
def audit_report(rows):
for r in [r for r in rows if not r.complete or r.mismatch or r.generated]:
print(f" [{r.section}] {r.task_id} ({'; '.join(reasons)})")
for src in r.raw.splitlines(): print(f" {src}")
migrate() also runs ALTER TABLE tasks ADD COLUMN … for any missing columns, so an existing DB produced by the old script is upgraded in place instead of erroring.
**Reproduced the old drops first** (test `test_old_regex_would_have_dropped_them`): on the sample board, the old parser (`^([A-Z]+-\d+)` + Active-only sections) kept exactly `[TASK-0012, TASK-0044, TASK-0055, BUG-2002]` — 5 of 9 rows silently dropped, including all of `## Dogfood Findings`. **After the fix** — CLI run on `board.md`: ``` DIFF (Tasks found vs manual row count) OK Active Sprint [Active*]: found=5 manual=5 OK Dogfood Findings: found=3 manual=3 OK Backlog: found=1 manual=1 => all sections in sync ``` DB contents: all **9/9 rows** present; `BUG-E2E-001a` and `BUG-E2E-002b` migrated; `TASK-0044` (✅ in title, `status: In Progress`) is `complete=False` with `mismatch=True`; `TASK-0055` (`[x]` but no status) is flagged for eyeball; the ID-less row became `UNPARSED-Dogfood-Findings-001`. **Backfill recovery demo** (DB that already lost rows to the old parser, old 2-column schema): migrate upgraded the schema, upserted all 9; after a simulated re-drop of `TASK-0099`, `backfill_missing` restored it and wrote an `migration_audit` row — total back to 9, diff clean. **Edge cases covered by the 22 passing pytest tests** (`~/board-migration/test_migration.py`): - lowercase + alpha-suffix IDs (`BUG-E2E-001a`, `BUG-E2E-002b`, `TASK-001a`) - ✅ before the ID (`✅ BUG-E2E-009c`) and after the ID - non-Active sections parsed with section names stored; empty sections count 0 - ✅ in title vs status column (not marked complete; mismatch flagged) - `[x]` without status → audit; genuinely Done rows not flagged - ID-less rows get placeholders, never dropped - duplicate IDs across sections counted distinctly - CRLF endings + trailing whitespace; nested sub-bullets not mistaken for rows - idempotency: migrate/backfill run twice → no duplicates (upsert)
{"model": "deepseek-v4-flash", "problem_class": "board-duckdb-migration-parser-drop", "result": "passed", "tests": 22}