MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-1652

Planning gap: status-derivation story was scoped upward-only (missed the symmetric downward cascade)

Done
Description

Planning-bug record (logged by motir re-plan [MOTIR-1615](motir:cmr7r0ryt000604k00njnms0d), 2026-07-05). Lesson: notes.html #149 (verified by content on origin/main, 2026-08-04 — the card originally cited no entry number).

The gap

MOTIR-1615 was planned as upward-only status rollup (children → parent) — it even documented what it does NOT do — without considering the symmetric downward direction (a done parent completing its children). Automatic status derivation between a parent and its children is inherently bidirectional; scoping only one direction is a completeness-axis gap (a real-product status-sync feature keeps parent and children consistent BOTH ways). Surfaced by the user's re-plan directive: "when the parent is marked done by CI, all children should be marked done too."

Fix applied (the re-plan)

Story re-scoped to bidirectional status derivation; added the downward-cascade service MOTIR-1647; the ADR MOTIR-1616, design MOTIR-1617, toggle field MOTIR-1618, Inngest job MOTIR-1621, settings UI MOTIR-1622, and both test subtasks MOTIR-1623 / MOTIR-1624 were swept to cover both directions.

Verified precondition (rung 2) — ACCURATE WHEN WRITTEN, FALSIFIED SINCE

As written 2026-07-06: workItemsService.applyStatusTransition (lib/services/workItemsService.ts:1489) is strictly canTransition-gated (IllegalTransitionError under restricted) with NO force/bypass, and the default workflow has no todo→done/blocked→done edge ⇒ the downward cascade cannot ride the legal transition graph and must ADD a privileged system-set path.

Re-executed at close-out against the card's own creation commit (92c3fe2d, the origin/main tip at 2026-07-06T01:18Z): async applyStatusTransition( was at :1489, and grep -c "system?: boolean" over that file returned 0. The claim was exact when made. Both halves have drifted since — see below.

Open decisions — ALL THREE SETTLED by the ADR MOTIR-1616

Recorded in docs/decisions/status-derivation.md (Accepted 2026-08-03; filed under docs/decisions/, not the card's docs/adr/ — rung 2, all 22 sibling ADRs live there):

  1. Toggle model → §2: two independent Project booleans, autoRollupParentStatus + autoCompleteChildrenOnParentDone, both default ON (recommendation adopted). One combined toggle rejected because it cannot express upward-only, the commonest preference.
  2. Downward mechanism → §4: opts.system (recommendation adopted in direction, corrected in premise) — the bypass is REUSED, not added. See the correction below.
  3. Rollout for existing projects → §6: backfill ON (recommendation adopted), column default only, no retroactive derivation job.

Close-out verification (2026-08-04, rung 2 against origin/main)

Shipped: 010ad0e8 feat(work-items): bidirectional parent↔child status derivation (MOTIR-1615) (#1816) (one Story→main PR) + 91d6f18e design(projects): … status-automation switches (MOTIR-1617) (#1815). Story and all nine linked cards are done.

  • applyStatusTransition is now at lib/services/workItemsService.ts:1629 (was :1489; the ADR quoted :1609 at its own HEAD 3d7a9831) — ordinary line rot, re-locate by content.
  • The "NO force/bypass" claim is now FALSE, and the "must ADD" conclusion with it. opts.system?: boolean landed 2026-07-08 — two days after this card — via 212df810 feat(import): persist engine + import API routes (MOTIR-941) (#1566), whose setImportedStatus (:1578) is its first caller. grep -c "system?: boolean" now returns 1. The ADR §4 caught this at run time: "The card proposed extending applyStatusTransition with opts.system; it already exists."
  • Right conclusion, wrong reason. The mechanism the card argued for held exactly — a privileged system set, NOT new user-draggable todo→done transition rows. lib/services/childStatusCascadeService.ts:133 passes { system: true } into the shared authority and adds no new bypass; its comment at :28 says so.
  • Toggles shipped: prisma/schema.prisma:713 / :722, both @default(true). MOTIR-1625's edge shipped at lib/workflows/defaultWorkflow.ts:88 (['in_progress', 'done']) and is present on the live tenant's workflow.

Drift class worth naming: a negative rung-2 claim can be correctly executed and true when written, then falsified by an unrelated sibling merge days later. Kept as a record note, not promoted and not written as a new notes.html entry — one occurrence, zero damage (the ADR self-corrected it downstream), and the standing defense is the one already in the corpus: re-execute every negative claim at close-out and paste the count, which is exactly what caught it here. notes.html #149 is left untouched: notes entries are historical records, and its lesson ("re-verify the reverse direction's mechanism against shipped reality") is strengthened by this, not contradicted.

No code action beyond the re-planned subtasks. Closed out on the ADR recording all three decisions.