Skip to content

moooon

Motir

Vibe your whole project. Bring an idea — Motir's three AI layers plan it, track it, and ship it, end to end. You're looking at Motir, built in Motir.

  • Vibe Project
  • Open Source
  • AI Agent
  • AI Loop
1
requests
0
upvotes
145
planned
1,361
shipped

Motir · Work items

MOTIR-3343Done

recordRecurrence re-enables a lesson a human switched OFF — the clock silently overrides the person

Live on origin/main, and the reason the retire feature cannot be built on enabled as it stands.

// the human switch — lessonService exposes it, the admin view calls it
setEnabled(db, id, enabled) → db.lesson.update({ where: { id }, data: { enabled } })

// the recurrence path
recordRecurrence(db, id, at) → db.lesson.update({ data: { enabled: true, lastOccurredAt: at } })

// what enabled gates
listForInjection → WHERE "enabled" = true

enabled is the only manual off switch there is, and recordRecurrence sets it unconditionally back to true. So a human disables a lesson, the mistake it describes happens again — for any reason, related or not — and the lesson is silently switched back on and resumes being injected. Nobody is told, nothing errors, and the setting a person changed simply stops holding.

Ageing out and being switched off are different states and must stop sharing a field. Expiry is lastOccurredAt < retirementCutoff(); it is a read-time filter and reviving from it is correct and deliberate. A human decision is not a clock reading, and a recurrence must not reverse it.

Reproduce first — the defect is a sequence, not a crash: disable a lesson, confirm it is not injected, record a recurrence, and show it is injected again.

Carry the audit the sibling feature needs while the field is being reshaped: when it was switched off and by whom. The retire UI has to show that, and adding it here avoids a second migration over the same column.

⚠️ Amendment (2026-08-23) — the state model is a THREE-VALUED humanOverride, not a second boolean

This card was authored 2026-08-21. The parent story MOTIR-3330 was amended by Yue on 2026-08-23 with an exempt from retirement decision that settles the shape of the very column this card adds, so the amendment outranks the card's original wording and is applied here rather than discovered at build time.

The story decides the two rules are ONE mechanism with two values, not two unrelated booleans"two independent flags invite the fourth state nobody has defined, and let a later edit satisfy one rule while breaking the other." So this card ships:

  • humanOverride: retired | exempt | unset (the default — the clock decides).
    • retired — never injected, and a recurrence does not revive it.
    • exempt — never dropped by the lastOccurredAt >= staleCutoff predicate, whatever the clock says.
  • humanOverrideAt / humanOverrideBy — the audit this card already owed ("when it was switched off and by whom"), now covering both directions of the one decision.
  • enabled stays exactly what it is — the operator switch. This is the axis beside it, not a replacement.

The story's own criterion "Nothing writes lastOccurredAt or recurrenceCount to express a human decision" is discharged here: the rejected alternative was bumping the clock to express Apply again, and MOTIR-3326 writes the clock and the count together precisely so they cannot disagree.

Acceptance criteria

  • A failing test reproduces the sequence first: disabled → not injected → recurrence → injected again.
  • After the fix, a recurrence bumps lastOccurredAt and does not re-enable a lesson a human switched off.
  • A recurrence still revives a lesson that merely aged out — that behaviour is preserved and asserted, so the fix does not close both doors.
  • The row records when it was switched off and by whom; the existing rows read as never-switched-off with no backfill.
  • The humanOverride axis carries all three values, and the injection read honours BOTH directions — a retired lesson is excluded whatever its clock says, and an exempt lesson survives the staleness predicate whatever its clock says. Asserted at the query.
  • recordRecurrence writes no humanOverride, asserted: a recurrence is an occurrence, never a decision.
  • The migration is forward-only and applies cleanly; prisma generate clean.
  • Every existing caller of setEnabled and recordRecurrence is enumerated in the PR body with its disposition — the grep and its hit count quoted.

Context refs

  • motir-ai src/repositories/lessonRepository.tssetEnabled, recordRecurrence, the WHERE "enabled" = true clause and the lastOccurredAt >= staleCutoff clause beside it.
  • motir-ai src/services/lessonService.ts — the service wrapper over setEnabled, and captureMistake's reinforce branch that calls recordRecurrence.
  • MOTIR-3323 — global reinforcement; both cards edit the revival path and should agree on its shape.