From 56e93b8cfbbc08130ff7e9c056444f5f3c35e6be Mon Sep 17 00:00:00 2001 From: Natalie Date: Mon, 29 Jun 2026 07:12:14 -0400 Subject: [PATCH] feat(service): teach-loop corrections + MCP tool (vetting + OSS tuning data) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit POST/GET prospector/corrections (own-DB prospect_corrections): operator/coworker records where the backend's decision or draft was wrong — the trial vetting signal and the substrate for tuning the OSS models. Adds prospector_correction MCP tool. Disable tsc incremental (nest deleteOutDir + stale tsbuildinfo dropped emitted files). tsc clean; corrections POST/GET verified live. Co-Authored-By: Claude Opus 4.8 (1M context) --- @packages/mcp-prospector/src/client.ts | 10 ++++ @packages/mcp-prospector/src/index.ts | 18 +++++++ migrations/0003_corrections.sql | 14 +++++ src/app.module.ts | 2 + src/corrections/corrections.controller.ts | 27 ++++++++++ src/corrections/corrections.module.ts | 14 +++++ src/corrections/corrections.service.ts | 54 ++++++++++++++++++++ src/corrections/dto/create-correction.dto.ts | 30 +++++++++++ src/entities/index.ts | 5 +- src/entities/prospect-correction.entity.ts | 35 +++++++++++++ tsconfig.json | 2 +- 11 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 migrations/0003_corrections.sql create mode 100644 src/corrections/corrections.controller.ts create mode 100644 src/corrections/corrections.module.ts create mode 100644 src/corrections/corrections.service.ts create mode 100644 src/corrections/dto/create-correction.dto.ts create mode 100644 src/entities/prospect-correction.entity.ts diff --git a/@packages/mcp-prospector/src/client.ts b/@packages/mcp-prospector/src/client.ts index 24ff892..144f4b3 100644 --- a/@packages/mcp-prospector/src/client.ts +++ b/@packages/mcp-prospector/src/client.ts @@ -33,6 +33,16 @@ export class ProspectorClient { return this.get(`/prospector/digest?sinceHours=${sinceHours}`); } + submitCorrection(body: { + handle: string; + category: string; + summary?: string; + originalBody?: string; + correctedBody?: string; + }): Promise { + return this.post('/prospector/corrections', body); + } + getSettings(): Promise { return this.get('/prospector/settings'); } diff --git a/@packages/mcp-prospector/src/index.ts b/@packages/mcp-prospector/src/index.ts index 31957ae..e1ad80e 100644 --- a/@packages/mcp-prospector/src/index.ts +++ b/@packages/mcp-prospector/src/index.ts @@ -62,6 +62,24 @@ server.registerTool( async ({ sinceHours }) => json(await client.getDigest(sinceHours ?? 12)), ); +server.registerTool( + 'prospector_correction', + { + title: 'Record a teach-loop correction', + description: + "Record a correction when the backend's decision/draft was wrong — the vetting signal during the trial and the tuning data for the OSS models.", + inputSchema: { + handle: z.string(), + category: z.enum(['voice', 'tone', 'safety', 'logistics', 'classification', 'other']), + summary: z.string().optional(), + originalBody: z.string().optional().describe('What the backend produced'), + correctedBody: z.string().optional().describe('What it should have been'), + }, + }, + async ({ handle, category, summary, originalBody, correctedBody }) => + json(await client.submitCorrection({ handle, category, summary, originalBody, correctedBody })), +); + server.registerTool( 'prospector_get_mode', { diff --git a/migrations/0003_corrections.sql b/migrations/0003_corrections.sql new file mode 100644 index 0000000..ed26186 --- /dev/null +++ b/migrations/0003_corrections.sql @@ -0,0 +1,14 @@ +-- prospector service — teach-loop corrections (own DB, black:25462). +-- Vetting signal during the trial + training substrate for OSS model tuning. + +CREATE TABLE IF NOT EXISTS prospect_corrections ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + handle TEXT NOT NULL, + category TEXT NOT NULL, -- voice | tone | safety | logistics | classification | other + summary TEXT, + original_body TEXT, -- what the backend produced + corrected_body TEXT, -- what it should have been + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); +CREATE INDEX IF NOT EXISTS idx_prospect_corrections_created ON prospect_corrections(created_at DESC); +CREATE INDEX IF NOT EXISTS idx_prospect_corrections_category ON prospect_corrections(category, created_at DESC); diff --git a/src/app.module.ts b/src/app.module.ts index 500feca..f3ccdab 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -7,6 +7,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { ServiceTokenGuard } from './auth/service-token.guard.js'; import { AuditModule } from './audit/audit.module.js'; import { ClientsModule } from './clients/clients.module.js'; +import { CorrectionsModule } from './corrections/corrections.module.js'; import { databaseConfig } from './config/database.config.js'; import { HealthModule } from './health/health.module.js'; import { InboundModule } from './inbound/inbound.module.js'; @@ -24,6 +25,7 @@ import { SettingsModule } from './settings/settings.module.js'; SettingsModule, // GO/PAUSE/AWAY kill-switch + settings RunnerModule, // the AFK decision pipeline (state -> Gate-2 -> mode) AuditModule, // decision audit trail + activity/held/digest endpoints + CorrectionsModule, // teach-loop corrections (vetting signal + OSS tuning data) InboundModule, // event-driven webhook: macsync inbound -> classify -> decide -> dispatch SchedulerModule, // in-service AFK heartbeat / follow-up tick ], diff --git a/src/corrections/corrections.controller.ts b/src/corrections/corrections.controller.ts new file mode 100644 index 0000000..e45c0b6 --- /dev/null +++ b/src/corrections/corrections.controller.ts @@ -0,0 +1,27 @@ +import { Body, Controller, DefaultValuePipe, Get, HttpCode, ParseIntPipe, Post, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; + +import { CreateCorrectionDto } from './dto/create-correction.dto.js'; +import { CorrectionsService } from './corrections.service.js'; + +/** + * Teach-loop corrections. The trial coworker (and operator) record disagreements + * with the backend's decisions/drafts here — the vetting signal and the OSS + * model-tuning substrate. (Inbound auth via the global guard.) + */ +@ApiTags('corrections') +@Controller('prospector/corrections') +export class CorrectionsController { + constructor(private readonly corrections: CorrectionsService) {} + + @Post() + @HttpCode(201) + async create(@Body() dto: CreateCorrectionDto) { + return this.corrections.create(dto); + } + + @Get() + async list(@Query('limit', new DefaultValuePipe(100), ParseIntPipe) limit: number) { + return { items: await this.corrections.list(limit) }; + } +} diff --git a/src/corrections/corrections.module.ts b/src/corrections/corrections.module.ts new file mode 100644 index 0000000..c655b83 --- /dev/null +++ b/src/corrections/corrections.module.ts @@ -0,0 +1,14 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { ProspectCorrectionEntity } from '../entities/index.js'; +import { CorrectionsController } from './corrections.controller.js'; +import { CorrectionsService } from './corrections.service.js'; + +@Module({ + imports: [TypeOrmModule.forFeature([ProspectCorrectionEntity])], + controllers: [CorrectionsController], + providers: [CorrectionsService], + exports: [CorrectionsService], +}) +export class CorrectionsModule {} diff --git a/src/corrections/corrections.service.ts b/src/corrections/corrections.service.ts new file mode 100644 index 0000000..c479f9a --- /dev/null +++ b/src/corrections/corrections.service.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; + +import { ProspectCorrectionEntity } from '../entities/index.js'; +import type { CreateCorrectionDto } from './dto/create-correction.dto.js'; + +export interface CorrectionItem { + readonly id: string; + readonly handle: string; + readonly category: string; + readonly summary: string | null; + readonly originalBody: string | null; + readonly correctedBody: string | null; + readonly createdAt: string; +} + +const toItem = (r: ProspectCorrectionEntity): CorrectionItem => ({ + id: r.id, + handle: r.handle, + category: r.category, + summary: r.summary, + originalBody: r.original_body, + correctedBody: r.corrected_body, + createdAt: r.created_at.toISOString(), +}); + +@Injectable() +export class CorrectionsService { + constructor( + @InjectRepository(ProspectCorrectionEntity) private readonly repo: Repository, + ) {} + + async create(dto: CreateCorrectionDto): Promise { + const saved = await this.repo.save( + this.repo.create({ + handle: dto.handle, + category: dto.category, + summary: dto.summary ?? null, + original_body: dto.originalBody ?? null, + corrected_body: dto.correctedBody ?? null, + }), + ); + return toItem(saved); + } + + async list(limit: number): Promise { + const rows = await this.repo.find({ + order: { created_at: 'DESC' }, + take: Math.min(Math.max(limit, 1), 500), + }); + return rows.map(toItem); + } +} diff --git a/src/corrections/dto/create-correction.dto.ts b/src/corrections/dto/create-correction.dto.ts new file mode 100644 index 0000000..e5968d0 --- /dev/null +++ b/src/corrections/dto/create-correction.dto.ts @@ -0,0 +1,30 @@ +import { IsIn, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; + +export const CORRECTION_CATEGORIES = ['voice', 'tone', 'safety', 'logistics', 'classification', 'other'] as const; +export type CorrectionCategory = (typeof CORRECTION_CATEGORIES)[number]; + +/** Body for POST /prospector/corrections — a teach-loop correction of a decision/draft. */ +export class CreateCorrectionDto { + @IsString() + @MinLength(1) + @MaxLength(256) + handle!: string; + + @IsIn(CORRECTION_CATEGORIES as readonly string[]) + category!: CorrectionCategory; + + @IsOptional() + @IsString() + @MaxLength(2000) + summary?: string; + + @IsOptional() + @IsString() + @MaxLength(8000) + originalBody?: string; + + @IsOptional() + @IsString() + @MaxLength(8000) + correctedBody?: string; +} diff --git a/src/entities/index.ts b/src/entities/index.ts index f8b57a4..ae2e89d 100644 --- a/src/entities/index.ts +++ b/src/entities/index.ts @@ -1,8 +1,9 @@ +import { ProspectCorrectionEntity } from './prospect-correction.entity.js'; import { ProspectDraftEntity } from './prospect-draft.entity.js'; import { ProspectorSettingsEntity } from './prospector-settings.entity.js'; -export const entities = [ProspectorSettingsEntity, ProspectDraftEntity] as const; +export const entities = [ProspectorSettingsEntity, ProspectDraftEntity, ProspectCorrectionEntity] as const; -export { ProspectorSettingsEntity, ProspectDraftEntity }; +export { ProspectorSettingsEntity, ProspectDraftEntity, ProspectCorrectionEntity }; export type { ProspectorMode } from './prospector-settings.entity.js'; export { PROSPECTOR_MODES } from './prospector-settings.entity.js'; diff --git a/src/entities/prospect-correction.entity.ts b/src/entities/prospect-correction.entity.ts new file mode 100644 index 0000000..f5e3502 --- /dev/null +++ b/src/entities/prospect-correction.entity.ts @@ -0,0 +1,35 @@ +import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; + +/** + * Teach-loop record: an operator/coworker correction of a backend decision or + * draft. This is the vetting signal during the trial AND the training substrate + * for tuning the OSS models (classification / draft). One row per correction. + */ +@Entity({ name: 'prospect_corrections' }) +@Index('idx_prospect_corrections_created', ['created_at']) +@Index('idx_prospect_corrections_category', ['category', 'created_at']) +export class ProspectCorrectionEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Column({ type: 'text' }) + handle!: string; + + /** What kind of correction: voice | tone | safety | logistics | classification | other. */ + @Column({ type: 'text' }) + category!: string; + + @Column({ type: 'text', nullable: true }) + summary!: string | null; + + /** What the backend produced (the draft/decision being corrected). */ + @Column({ name: 'original_body', type: 'text', nullable: true }) + original_body!: string | null; + + /** What it should have been. */ + @Column({ name: 'corrected_body', type: 'text', nullable: true }) + corrected_body!: string | null; + + @CreateDateColumn({ name: 'created_at', type: 'timestamptz' }) + created_at!: Date; +} diff --git a/tsconfig.json b/tsconfig.json index 497fba0..7514e1e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "strict": true, "declaration": false, "sourceMap": true, - "incremental": true, + "incremental": false, "esModuleInterop": true, "skipLibCheck": true, "emitDecoratorMetadata": true,