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-3359Done

motir-ai: POST /v1/lessons — create ONE tenant lesson, embedded, never global

The write half of the lessons surface: one route in src/app.ts, serviceAuth like every other /v1 route, creating a single tenant lesson for one project.

It must go through lessonService.createFromCorrection. That function embeds the text and inserts the row in one transaction. A route that inserts directly — or via SQL — leaves embedding NULL, and listForInjection's WHERE … embedding IS NOT NULL then drops the row from every query forever. The write returns 201, the lesson is in the table, the admin list shows it, and the planner never once sees it. Nothing anywhere reports a failure. This is the single way this card can be built wrong.

Global is refused. scope is not an input; the route creates tenant rows only. A global lesson is the product's curated corpus and is added by migration, never by a call. If the body carries scope: "global", that is a validation_error, not a silently-ignored field.

A near-duplicate is reported, not folded. captureMistake treats a tenant correction within CAPTURE_DEDUP_DISTANCE (0.05) of an existing lesson as a recurrence and reinforces that one instead of inserting. That is right for automatic capture and wrong here: a caller deliberately adding a lesson would get back a different lesson's id and have no way to know. Refuse instead, naming the existing lesson's id and title, so the caller can reword it, or go retire the one that is already there.

Accepts the three routing axes — kinds, types, phases — and stores them. Empty on an axis means unconstrained, so an untagged lesson reaches every query; that is the store's rule and this route does not second-guess it.

sourceRef carries provenance and buys idempotency for free: it is unique, and createFromCorrection already re-reads on a lost race. Give a hand-added lesson a sourceRef that marks it as such, so the detail view can distinguish the planner concluded this from someone wrote this down.

The project comes in the way the sibling read route takes it (MOTIR-3335) — same identifier convention, core's id mapped to aiProjectId. Do not invent a second one.

Acceptance criteria

  • POST /v1/lessons exists in src/app.ts under serviceAuth, alongside the sibling read route.
  • It calls lessonService.createFromCorrection; a test asserts the created row's embedding is non-null.
  • A test asserts the created lesson comes back from selectForInjection for that project — existence in the table is not the assertion, retrievability is.
  • The same lesson does NOT come back for a different project.
  • A body naming scope: "global" is refused with a validation_error; no row is written.
  • A create within CAPTURE_DEDUP_DISTANCE of an existing tenant lesson is refused, and the response names the existing lesson's id and title.
  • kinds, types and phases round-trip; an omitted axis stores empty and the lesson still matches an axis-filtered query.
  • A repeat with the same sourceRef returns the existing lesson rather than a second row.
  • A body missing title, body or howToApply is refused by assertContent's existing rule, not a new one.

Context refs

  • motir-ai src/app.ts — the /v1 route table and serviceAuth; POST /v1/embeddings is the shape to copy.
  • motir-ai src/services/lessonService.tscreateFromCorrection, insertWithEmbedding, assertContent, CAPTURE_DEDUP_DISTANCE, findNearestTenant.
  • motir-ai src/repositories/lessonRepository.tslistForInjection's embedding IS NOT NULL clause, the reason the embedding is not optional.
  • motir-ai prisma/schema.prismaLessonWorkItemKind, LessonWorkType, LessonPlanPhase, MistakeType.
  • MOTIR-3335 — the sibling GET /v1/lessons; take its project-identifier convention.