Skip to content

fix(helm): add missing and deleted-card phases to Python sync planner - #31

Merged
geojitsu merged 5 commits into
mainfrom
fm/fm-helm-sync-python-missing-deleted-001
Sep 18, 2026
Merged

geojitsu merged 5 commits into
mainfrom
fm/fm-helm-sync-python-missing-deleted-001

Conversation

@geojitsu

Copy link
Copy Markdown
Owner

Intent

Found by the live canary-board check before merging PR #30 (already merged; see data/fm-helm-sync-canary-board-002/report.md finding 4 and 5 for the full evidence). The captain's own follow-up ask, relayed: get the Python Helm sync planner's two missing phases built and re-verified before anyone attempts to switch bin/fm-helm-sync.sh over to it.

What Changed

  • Added a "missing" phase to plan_board that closes orphaned Helm cards (or, for a never-before-seen captain-created card, wakes intake instead) mirroring the bash/jq planner's missing_entries logic, guarded so an empty desired set touches no cards.
  • Added a "deleted" phase that either silently retains a tombstone (task already Done or under an existing captain hold) or places a captain hold with a wake request when a cached card's item disappears from the board with no replacement, mirroring deleted_entries.
  • Extended DesiredCard/SyncState/HoldDeletedTask with hold_kind, cache_existed, and item-aware fields, added WakeMissingCard/SkipRecreatingDeletedCard plan actions with jq-parity byte serialization, and updated the architecture/API docs plus jq-parity tests to cover the new phases.

Risk Assessment

✅ Low: The cumulative planner changes are bounded and the reviewed missing/deleted-card paths match the production jq decisions at the modeled fixture boundary.

Testing

Completed 1 recorded test check.

  • Outcome: ⏭️ skipped across 1 run (29m21s)

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 2 issues found → auto-fixed (4) ✅
  • 🚨 python/helm_sync/planner.py:614 - _missing_phase_actions takes only snapshot and desired_by_task - it has no access to state.cards (the port's equivalent of jq's old_by_task). Its own docstring (lines 617-623) claims a 'valid but never-before-seen task id (a captain-created card with no backlog task)' is 'left untouched', matching jq's 'ignore'/'wake: run intake' branches - but the code has no way to make that distinction and unconditionally appends CloseMissingCard (a live Status->Done field write) for every card whose task isn't in desired_by_task and isn't already Done. In bin/fm-helm-lib.sh (missing_entries, lines 770-775), a task id never seen in $old_by_task produces action 'wake' (send an intake notification) with NO field write at all - the card is left alone. Concrete failing sequence: a captain manually adds a draft card to the board with a well-formed backtick task id that has never existed in the backlog (this is exactly the scenario jq's own comment anticipates: 'captain added Helm card ... with no backlog task; run intake'). On the next plan_board call, this port marks that card Done instead of leaving it untouched - a wrong, silently-destructive action for a card the backlog never owned. docs/1.architecture/3.helm-sync-python.md describes only the missing wake notification ('does not yet raise jq's ... wake') without disclosing that the fallback also performs an incorrect field write, so this isn't a knowingly-scoped simplification as documented - it's a materially different, undisclosed behavior change. Since the user intent for this change is specifically to get the two missing-card phases 'built and re-verified' before any cutover, this parity gap in exactly that phase is in scope now, not deferred.
  • ⚠️ python/helm_sync/planner.py:677 - _deleted_phase_actions only retains a deleted card silently (KeepDeletedTombstone) when str(wanted.status) == 'Done'. jq's deleted_entries (bin/fm-helm-lib.sh:794) also retains silently when $rec.hold_kind == 'captain' (an existing captain hold), independent of task state. DesiredCard carries no hold_kind/hold_reason field, so a captain-held task (hold_kind=='captain', hold_reason set, state != done) renders to status 'Waiting on you' via desired.py's _status()/_kind(), never 'Done' - meaning this port will raise a new captain-hold wake for a card deletion that jq would silently retain, since the task is already under a captain hold. The smallest honest fix requires adding a hold_kind/hold_reason field to DesiredCard (a model/schema extension), so this is flagged for the author to decide rather than to auto-fix.

🔧 Fix: fix(helm): wake intake for unseen orphan cards and retain captain-held deletions
2 warnings still open:

  • ⚠️ python/helm_sync/planner.py:630 - The fix correctly stops closing never-before-seen orphan cards (round-1 destructive bug is resolved), but it still isn't full parity with jq's missing_entries: jq only takes the wake branch when $tsv_existed == "true" AND $old_by_task[$tid] == null (bin/fm-helm-lib.sh:770); when the identity cache never existed before this run ($tsv_existed false, e.g. the very first sync of a brand-new board), jq closes the orphan card unconditionally regardless of old_by_task. Python's SyncState has no field equivalent to tsv_existed (model.py:301-310), so task not in state.cards cannot distinguish 'cache exists but never saw this task' from 'cache never existed at all' -- every orphan card on a board's first-ever sync now wakes intake instead of closing, diverging from jq. This is narrow (only the literal first invocation for a board, with a pre-existing orphan draft already on it) and safe-direction (wakes instead of silently closing), but it is a real, source-verifiable parity gap the intent's re-verification goal is meant to catch. The honest fix requires adding a durable tsv_existed-equivalent flag to SyncState (a schema/state extension), so this needs the author's decision rather than a mechanical patch.
  • ⚠️ docs/1.architecture/3.helm-sync-python.md:40 - This line still says the planner 'does not yet raise jq's separate "captain added a card with no backlog task, run intake" wake', but this fix commit implements exactly that wake via WakeMissingCard (planner.py:642-655). The doc was not updated in this round and now misdescribes already-implemented behavior to anyone using it to judge cutover readiness.

🔧 Fix: fix(helm): close orphan cards on first-ever sync, not wake
1 warning still open:

  • ⚠️ docs/1.architecture/3.helm-sync-python.md:39 - This finding was selected as user_chose_to_fix in the previous round but the doc file has zero diff in this round's commit (55e8487). Line 39 still says the planner 'does not yet raise jq's separate captain-added-card wake', but WakeMissingCard (planner.py:637-655) has implemented exactly that wake since the prior commit (27ce559). The doc misdescribes already-shipped behavior to anyone judging cutover readiness against the stated intent.

🔧 Fix: docs(helm): fix stale note on implemented intake wake
2 warnings still open:

  • ⚠️ python/helm_sync/planner.py:625 - _missing_phase_actions has no equivalent of jq's missing_entries guard if ($records | length) == 0 then [] else ... (bin/fm-helm-lib.sh:761). jq refuses to touch any board card when the full desired/backlog record set is empty, specifically to avoid mass-closing or mass-waking every card on the board if the backlog fetch failed or returned nothing. The Python port has no such guard: if desired_by_task comes back empty for a board (e.g. an upstream backlog-fetch failure once this planner is wired into the real sync), every non-Done card on that board is treated as an orphan and gets CloseMissingCard (Status->Done) or WakeMissingCard, silently and incorrectly reconciling the whole board instead of doing nothing. This is the same class of destructive-fallback bug flagged and fixed in round 1 (missing-phase-closes-never-before-seen-card), just triggered by a different precondition (empty records vs. never-seen task) that the fix rounds did not add a guard for. The smallest honest remedy is mechanical: short-circuit _missing_phase_actions (e.g. if not desired_by_task: return [], threaded from the pre-board-filter desired set to match jq's global $records check) — no new state or schema is needed.
  • ⚠️ docs/2.api/3.helm-sync-python.md:343 - This file still says 'It does not yet raise the separate jq intake wake for a captain-created card with no matching task' and its type list/entries (lines 166-183, 341) omit WakeMissingCard entirely, even though WakeMissingCard (planner.py:637-655) has implemented exactly that wake since commit 27ce559. Round 3's fix (b061c71) corrected the identical stale claim in the sibling doc docs/1.architecture/3.helm-sync-python.md but did not touch this API doc, so the same misdescription of already-shipped behavior persists here.

🔧 Fix: fix(helm): guard empty-backlog missing phase, sync API docs
✅ Re-checked - no issues remain.

⏭️ **Test** - skipped
  • 🚨 tests failed with exit code 1
  • bin/fm-test-run.sh --changed --exclude-family real-herdr-gated
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

firstmate-crewmate added 5 commits September 18, 2026 00:17
…nc planner

The canary check before merging PR #30 found that plan_board() only
processed cards already in the desired set, so a board card whose task
left every backlog was never closed, and a captain-deleted card on a
still-live task was silently recreated instead of held. Both phases
now mirror bin/fm-helm-lib.sh's missing_entries and deleted_entries,
proven byte-for-byte against the production jq planner with fixture
data only (fixture-owner, boards 999/1000).

Known remaining gaps, documented in docs/1.architecture and
docs/2.api: the jq side's malformed-body-ignore and new-card-intake
wake branches, dispatch-marker cleanup on a closed card, and
in-flight/blocked-specific hold wording are not yet ported.
@geojitsu
geojitsu merged commit 25a5c97 into main Sep 18, 2026
13 of 14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant