The HTTP door onto the ledger the previous card built, so motir-core can record a hand-fired hit across the 7.1 boundary.
POST /v1/lessons/:id/reinforce, under serviceAuth, in the /v1/lessons block of src/app.ts. A named act on a resource, POST rather than PATCH — the shape the retire / apply pair already established two routes above. Body: coreWorkspaceId, coreProjectId, occurrenceRef, optional source. It calls lessonService.reinforceLesson and returns the lesson plus whether this call was the one that counted, so a caller can tell a first record from a replay.
The obvious move is to reuse resolveOwnTenantLesson (src/services/lessonService.ts:1579), which retireTenantLessonByCoreIds (:1606) and applyTenantLessonByCoreIds (:1639) both call. Do not. That resolver is TENANT-ONLY by decision and refuses a global row as not_found on the existence-oracle posture — correct for those two, because retiring a global row from one customer's console would reach every other tenant.
Reinforcement is the opposite case. MOTIR-3323 established that a global lesson must be reinforceable, and the entire curated base corpus is global — a reinforce route that refuses global rows would leave this card's whole parent unable to touch the rows it exists for. So resolve with POST /v1/lessons/search's predicate instead: scope = 'global' OR (scope = 'tenant' AND aiProjectId = <caller>). Another tenant's row still raises not_found in the same words. Two questions, two resolvers, one store — the distinction src/app.ts already draws in prose between the tenant-only reads and the teaching read, applied to a write for the first time.
POST /v1/lessons/:id/reinforce exists under serviceAuth, keyed by coreWorkspaceId + coreProjectId exactly as its siblings are, and takes occurrenceRef as a required body field.resolveOwnTenantLesson would have refused.not_found, in the same words an unknown id does. Both asserted.occurrenceRef returns 200 with counted: false and leaves both counters unchanged — the ledger's guarantee, surfaced rather than hidden.resolveOwnTenantLesson is UNCHANGED and still tenant-only; a test pins that the retire and apply endpoints still refuse a global row, so this card cannot loosen them by accident./v1/lessons block with a comment saying WHY its resolver differs from the two above it.src/app.ts — the /v1/lessons block: the tenant-only reads, POST /v1/lessons/search (the both-scopes predicate to copy), and the retire / apply named acts (the request shape to copy).src/services/lessonService.ts — resolveOwnTenantLesson (:1579), retireTenantLessonByCoreIds (:1606), applyTenantLessonByCoreIds (:1639), searchLessonsByCoreIds.tests/lessonInspectionSurface.test.ts — where the narrower tenant-only predicate is already pinned; this card's "still refuses a global row" assertion belongs beside it.MOTIR-3547 (the parent) — the rule and the resolver trap, stated once.