A GLOBAL lesson can never be reinforced, so the whole curated base corpus goes invisible 90 days after seeding
Live defect, found while planning MOTIR-3296.
captureMistake routes by scope:
if (input.mistake.scope === 'global') {
lesson = await createFromBase({ ...content, sourceRef }, db); // no near-dup guard
} else {
// tenant: reinforce a near-identical existing lesson rather than duplicate it.
const nearest = await lessonRepository.findNearestTenant(…);
if (nearest && nearest.distance <= CAPTURE_DEDUP_DISTANCE) {
lesson = toDto(await lessonRepository.recordRecurrence(db, nearest.id, new Date()));
}
}
The reinforce path is tenant-only. The matcher is findNearestTenant and it is reached only from the else. A global mistake goes straight to createFromBase.
And listForInjection filters AND "lastOccurredAt" >= staleCutoff, with staleCutoff = retirementCutoff() = now − 90 days.
So for every global lesson: lastOccurredAt is written once at creation and nothing can ever bump it → 90 days later it falls out of every query → and because the only revival path (recordRecurrence) is unreachable for global scope, it never comes back.
The curated base corpus is global. The seed's own header describes "global rows for every tenant, tenant rows bound to Motir's own AiProject". So the base lessons — including the 195 MOTIR-3296 is about to add and the limbs MOTIR-3309 migrates — have a 90-day expiry with no renewal.
Reproduce before fixing. The defect is a time-travel assertion, not a crash: seed a global lesson, confirm it is returned, advance the clock past the cutoff, confirm it is gone, then attempt every existing path that could revive it and show none does.
Acceptance criteria
- A failing test reproduces it first: a global lesson seeded, returned, aged past the cutoff, absent — and no available call path restores it.
- A global recurrence reinforces the matching global lesson:
lastOccurredAtbumped,enabledre-asserted, no duplicate row created. - The matcher used for global scope searches the global set (
aiProjectId IS NULL), not the tenant set — a tenant-scoped nearest-neighbour cannot see a global row. - Tenant behaviour is unchanged, asserted against the existing suite.
- The dedup distance threshold is shared with the tenant path rather than re-tuned here, or the difference is stated with its reason.
- Whether already-expired rows are revived on deploy is decided explicitly and recorded — a one-off touch of
lastOccurredAt, or nothing, with the reason.
Context refs
motir-aisrc/services/lessonService.ts—captureMistake's scope branch,retirementCutoff,LESSON_RETENTION_DAYS_DEFAULT = 90.motir-aisrc/repositories/lessonRepository.ts—findNearestTenant,recordRecurrence, thelastOccurredAt >= staleCutoffclause.motir-aisrc/seed/seedBaseLessons.ts— where base lessons are created global.motir-aitests/lessonLifecycle.test.ts,tests/lessonInjection.test.ts.