feat: Persist review comments to database
Review comments on phase diffs now survive page reloads and phase switches. Adds review_comments table (migration 0028), repository port/adapter (13th repo), tRPC procedures (listReviewComments, createReviewComment, resolveReviewComment, unresolveReviewComment), and replaces useState-based comments in ReviewTab with tRPC queries and mutations.
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
DrizzleLogChunkRepository,
|
||||
DrizzleConversationRepository,
|
||||
DrizzleChatSessionRepository,
|
||||
DrizzleReviewCommentRepository,
|
||||
} from './db/index.js';
|
||||
import type { InitiativeRepository } from './db/repositories/initiative-repository.js';
|
||||
import type { PhaseRepository } from './db/repositories/phase-repository.js';
|
||||
@@ -34,6 +35,7 @@ import type { ChangeSetRepository } from './db/repositories/change-set-repositor
|
||||
import type { LogChunkRepository } from './db/repositories/log-chunk-repository.js';
|
||||
import type { ConversationRepository } from './db/repositories/conversation-repository.js';
|
||||
import type { ChatSessionRepository } from './db/repositories/chat-session-repository.js';
|
||||
import type { ReviewCommentRepository } from './db/repositories/review-comment-repository.js';
|
||||
import type { EventBus } from './events/index.js';
|
||||
import { createEventBus } from './events/index.js';
|
||||
import { ProcessManager, ProcessRegistry } from './process/index.js';
|
||||
@@ -58,7 +60,7 @@ import type { ServerContextDeps } from './server/index.js';
|
||||
// =============================================================================
|
||||
|
||||
/**
|
||||
* All 12 repository ports.
|
||||
* All 13 repository ports.
|
||||
*/
|
||||
export interface Repositories {
|
||||
initiativeRepository: InitiativeRepository;
|
||||
@@ -73,10 +75,11 @@ export interface Repositories {
|
||||
logChunkRepository: LogChunkRepository;
|
||||
conversationRepository: ConversationRepository;
|
||||
chatSessionRepository: ChatSessionRepository;
|
||||
reviewCommentRepository: ReviewCommentRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create all 12 Drizzle repository adapters from a database instance.
|
||||
* Create all 13 Drizzle repository adapters from a database instance.
|
||||
* Reusable by both the production server and the test harness.
|
||||
*/
|
||||
export function createRepositories(db: DrizzleDatabase): Repositories {
|
||||
@@ -93,6 +96,7 @@ export function createRepositories(db: DrizzleDatabase): Repositories {
|
||||
logChunkRepository: new DrizzleLogChunkRepository(db),
|
||||
conversationRepository: new DrizzleConversationRepository(db),
|
||||
chatSessionRepository: new DrizzleChatSessionRepository(db),
|
||||
reviewCommentRepository: new DrizzleReviewCommentRepository(db),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -17,3 +17,4 @@ export { DrizzleChangeSetRepository } from './change-set.js';
|
||||
export { DrizzleLogChunkRepository } from './log-chunk.js';
|
||||
export { DrizzleConversationRepository } from './conversation.js';
|
||||
export { DrizzleChatSessionRepository } from './chat-session.js';
|
||||
export { DrizzleReviewCommentRepository } from './review-comment.js';
|
||||
|
||||
78
apps/server/db/repositories/drizzle/review-comment.ts
Normal file
78
apps/server/db/repositories/drizzle/review-comment.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Drizzle Review Comment Repository Adapter
|
||||
*
|
||||
* Implements ReviewCommentRepository interface using Drizzle ORM.
|
||||
*/
|
||||
|
||||
import { eq, asc } from 'drizzle-orm';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { DrizzleDatabase } from '../../index.js';
|
||||
import { reviewComments, type ReviewComment } from '../../schema.js';
|
||||
import type { ReviewCommentRepository, CreateReviewCommentData } from '../review-comment-repository.js';
|
||||
|
||||
export class DrizzleReviewCommentRepository implements ReviewCommentRepository {
|
||||
constructor(private db: DrizzleDatabase) {}
|
||||
|
||||
async create(data: CreateReviewCommentData): Promise<ReviewComment> {
|
||||
const now = new Date();
|
||||
const id = nanoid();
|
||||
await this.db.insert(reviewComments).values({
|
||||
id,
|
||||
phaseId: data.phaseId,
|
||||
filePath: data.filePath,
|
||||
lineNumber: data.lineNumber,
|
||||
lineType: data.lineType,
|
||||
body: data.body,
|
||||
author: data.author ?? 'you',
|
||||
resolved: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
const rows = await this.db
|
||||
.select()
|
||||
.from(reviewComments)
|
||||
.where(eq(reviewComments.id, id))
|
||||
.limit(1);
|
||||
return rows[0]!;
|
||||
}
|
||||
|
||||
async findByPhaseId(phaseId: string): Promise<ReviewComment[]> {
|
||||
return this.db
|
||||
.select()
|
||||
.from(reviewComments)
|
||||
.where(eq(reviewComments.phaseId, phaseId))
|
||||
.orderBy(asc(reviewComments.createdAt));
|
||||
}
|
||||
|
||||
async resolve(id: string): Promise<ReviewComment | null> {
|
||||
await this.db
|
||||
.update(reviewComments)
|
||||
.set({ resolved: true, updatedAt: new Date() })
|
||||
.where(eq(reviewComments.id, id));
|
||||
const rows = await this.db
|
||||
.select()
|
||||
.from(reviewComments)
|
||||
.where(eq(reviewComments.id, id))
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
async unresolve(id: string): Promise<ReviewComment | null> {
|
||||
await this.db
|
||||
.update(reviewComments)
|
||||
.set({ resolved: false, updatedAt: new Date() })
|
||||
.where(eq(reviewComments.id, id));
|
||||
const rows = await this.db
|
||||
.select()
|
||||
.from(reviewComments)
|
||||
.where(eq(reviewComments.id, id))
|
||||
.limit(1);
|
||||
return rows[0] ?? null;
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.db
|
||||
.delete(reviewComments)
|
||||
.where(eq(reviewComments.id, id));
|
||||
}
|
||||
}
|
||||
@@ -78,3 +78,8 @@ export type {
|
||||
CreateChatSessionData,
|
||||
CreateChatMessageData,
|
||||
} from './chat-session-repository.js';
|
||||
|
||||
export type {
|
||||
ReviewCommentRepository,
|
||||
CreateReviewCommentData,
|
||||
} from './review-comment-repository.js';
|
||||
|
||||
24
apps/server/db/repositories/review-comment-repository.ts
Normal file
24
apps/server/db/repositories/review-comment-repository.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Review Comment Repository Port Interface
|
||||
*
|
||||
* Port for persisting inline review comments on phase diffs.
|
||||
*/
|
||||
|
||||
import type { ReviewComment } from '../schema.js';
|
||||
|
||||
export interface CreateReviewCommentData {
|
||||
phaseId: string;
|
||||
filePath: string;
|
||||
lineNumber: number;
|
||||
lineType: 'added' | 'removed' | 'context';
|
||||
body: string;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
export interface ReviewCommentRepository {
|
||||
create(data: CreateReviewCommentData): Promise<ReviewComment>;
|
||||
findByPhaseId(phaseId: string): Promise<ReviewComment[]>;
|
||||
resolve(id: string): Promise<ReviewComment | null>;
|
||||
unresolve(id: string): Promise<ReviewComment | null>;
|
||||
delete(id: string): Promise<void>;
|
||||
}
|
||||
@@ -604,3 +604,27 @@ export const chatMessagesRelations = relations(chatMessages, ({ one }) => ({
|
||||
|
||||
export type ChatMessage = InferSelectModel<typeof chatMessages>;
|
||||
export type NewChatMessage = InferInsertModel<typeof chatMessages>;
|
||||
|
||||
// ============================================================================
|
||||
// REVIEW COMMENTS
|
||||
// ============================================================================
|
||||
|
||||
export const reviewComments = sqliteTable('review_comments', {
|
||||
id: text('id').primaryKey(),
|
||||
phaseId: text('phase_id')
|
||||
.notNull()
|
||||
.references(() => phases.id, { onDelete: 'cascade' }),
|
||||
filePath: text('file_path').notNull(),
|
||||
lineNumber: integer('line_number').notNull(),
|
||||
lineType: text('line_type', { enum: ['added', 'removed', 'context'] }).notNull(),
|
||||
body: text('body').notNull(),
|
||||
author: text('author').notNull().default('you'),
|
||||
resolved: integer('resolved', { mode: 'boolean' }).notNull().default(false),
|
||||
createdAt: integer('created_at', { mode: 'timestamp' }).notNull(),
|
||||
updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull(),
|
||||
}, (table) => [
|
||||
index('review_comments_phase_id_idx').on(table.phaseId),
|
||||
]);
|
||||
|
||||
export type ReviewComment = InferSelectModel<typeof reviewComments>;
|
||||
export type NewReviewComment = InferInsertModel<typeof reviewComments>;
|
||||
|
||||
15
apps/server/drizzle/0028_add_review_comments.sql
Normal file
15
apps/server/drizzle/0028_add_review_comments.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
-- Add review_comments table for persisting inline review comments on phase diffs
|
||||
CREATE TABLE IF NOT EXISTS review_comments (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
phase_id TEXT NOT NULL REFERENCES phases(id) ON DELETE CASCADE,
|
||||
file_path TEXT NOT NULL,
|
||||
line_number INTEGER NOT NULL,
|
||||
line_type TEXT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
author TEXT NOT NULL DEFAULT 'you',
|
||||
resolved INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS review_comments_phase_id_idx ON review_comments (phase_id);
|
||||
@@ -21,6 +21,7 @@ import type { ChangeSetRepository } from '../db/repositories/change-set-reposito
|
||||
import type { LogChunkRepository } from '../db/repositories/log-chunk-repository.js';
|
||||
import type { ConversationRepository } from '../db/repositories/conversation-repository.js';
|
||||
import type { ChatSessionRepository } from '../db/repositories/chat-session-repository.js';
|
||||
import type { ReviewCommentRepository } from '../db/repositories/review-comment-repository.js';
|
||||
import type { AccountCredentialManager } from '../agent/credentials/types.js';
|
||||
import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js';
|
||||
import type { CoordinationManager } from '../coordination/types.js';
|
||||
@@ -76,6 +77,8 @@ export interface TrpcAdapterOptions {
|
||||
conversationRepository?: ConversationRepository;
|
||||
/** Chat session repository for iterative phase/task chat */
|
||||
chatSessionRepository?: ChatSessionRepository;
|
||||
/** Review comment repository for inline review comments on phase diffs */
|
||||
reviewCommentRepository?: ReviewCommentRepository;
|
||||
/** Absolute path to the workspace root (.cwrc directory) */
|
||||
workspaceRoot?: string;
|
||||
}
|
||||
@@ -158,6 +161,7 @@ export function createTrpcHandler(options: TrpcAdapterOptions) {
|
||||
previewManager: options.previewManager,
|
||||
conversationRepository: options.conversationRepository,
|
||||
chatSessionRepository: options.chatSessionRepository,
|
||||
reviewCommentRepository: options.reviewCommentRepository,
|
||||
workspaceRoot: options.workspaceRoot,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { ChangeSetRepository } from '../db/repositories/change-set-reposito
|
||||
import type { LogChunkRepository } from '../db/repositories/log-chunk-repository.js';
|
||||
import type { ConversationRepository } from '../db/repositories/conversation-repository.js';
|
||||
import type { ChatSessionRepository } from '../db/repositories/chat-session-repository.js';
|
||||
import type { ReviewCommentRepository } from '../db/repositories/review-comment-repository.js';
|
||||
import type { AccountCredentialManager } from '../agent/credentials/types.js';
|
||||
import type { DispatchManager, PhaseDispatchManager } from '../dispatch/types.js';
|
||||
import type { CoordinationManager } from '../coordination/types.js';
|
||||
@@ -76,6 +77,8 @@ export interface TRPCContext {
|
||||
conversationRepository?: ConversationRepository;
|
||||
/** Chat session repository for iterative phase/task chat */
|
||||
chatSessionRepository?: ChatSessionRepository;
|
||||
/** Review comment repository for inline review comments on phase diffs */
|
||||
reviewCommentRepository?: ReviewCommentRepository;
|
||||
/** Absolute path to the workspace root (.cwrc directory) */
|
||||
workspaceRoot?: string;
|
||||
}
|
||||
@@ -106,6 +109,7 @@ export interface CreateContextOptions {
|
||||
previewManager?: PreviewManager;
|
||||
conversationRepository?: ConversationRepository;
|
||||
chatSessionRepository?: ChatSessionRepository;
|
||||
reviewCommentRepository?: ReviewCommentRepository;
|
||||
workspaceRoot?: string;
|
||||
}
|
||||
|
||||
@@ -139,6 +143,7 @@ export function createContext(options: CreateContextOptions): TRPCContext {
|
||||
previewManager: options.previewManager,
|
||||
conversationRepository: options.conversationRepository,
|
||||
chatSessionRepository: options.chatSessionRepository,
|
||||
reviewCommentRepository: options.reviewCommentRepository,
|
||||
workspaceRoot: options.workspaceRoot,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import type { ChangeSetRepository } from '../../db/repositories/change-set-repos
|
||||
import type { LogChunkRepository } from '../../db/repositories/log-chunk-repository.js';
|
||||
import type { ConversationRepository } from '../../db/repositories/conversation-repository.js';
|
||||
import type { ChatSessionRepository } from '../../db/repositories/chat-session-repository.js';
|
||||
import type { ReviewCommentRepository } from '../../db/repositories/review-comment-repository.js';
|
||||
import type { DispatchManager, PhaseDispatchManager } from '../../dispatch/types.js';
|
||||
import type { CoordinationManager } from '../../coordination/types.js';
|
||||
import type { BranchManager } from '../../git/branch-manager.js';
|
||||
@@ -203,3 +204,13 @@ export function requireChatSessionRepository(ctx: TRPCContext): ChatSessionRepos
|
||||
}
|
||||
return ctx.chatSessionRepository;
|
||||
}
|
||||
|
||||
export function requireReviewCommentRepository(ctx: TRPCContext): ReviewCommentRepository {
|
||||
if (!ctx.reviewCommentRepository) {
|
||||
throw new TRPCError({
|
||||
code: 'INTERNAL_SERVER_ERROR',
|
||||
message: 'Review comment repository not available',
|
||||
});
|
||||
}
|
||||
return ctx.reviewCommentRepository;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { TRPCError } from '@trpc/server';
|
||||
import { z } from 'zod';
|
||||
import type { Phase } from '../../db/schema.js';
|
||||
import type { ProcedureBuilder } from '../trpc.js';
|
||||
import { requirePhaseRepository, requireTaskRepository, requireBranchManager, requireInitiativeRepository, requireProjectRepository, requireExecutionOrchestrator } from './_helpers.js';
|
||||
import { requirePhaseRepository, requireTaskRepository, requireBranchManager, requireInitiativeRepository, requireProjectRepository, requireExecutionOrchestrator, requireReviewCommentRepository } from './_helpers.js';
|
||||
import { phaseBranchName } from '../../git/branch-naming.js';
|
||||
import { ensureProjectClone } from '../../git/project-clones.js';
|
||||
|
||||
@@ -298,5 +298,48 @@ export function phaseProcedures(publicProcedure: ProcedureBuilder) {
|
||||
|
||||
return { rawDiff };
|
||||
}),
|
||||
|
||||
listReviewComments: publicProcedure
|
||||
.input(z.object({ phaseId: z.string().min(1) }))
|
||||
.query(async ({ ctx, input }) => {
|
||||
const repo = requireReviewCommentRepository(ctx);
|
||||
return repo.findByPhaseId(input.phaseId);
|
||||
}),
|
||||
|
||||
createReviewComment: publicProcedure
|
||||
.input(z.object({
|
||||
phaseId: z.string().min(1),
|
||||
filePath: z.string().min(1),
|
||||
lineNumber: z.number().int(),
|
||||
lineType: z.enum(['added', 'removed', 'context']),
|
||||
body: z.string().min(1),
|
||||
author: z.string().optional(),
|
||||
}))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const repo = requireReviewCommentRepository(ctx);
|
||||
return repo.create(input);
|
||||
}),
|
||||
|
||||
resolveReviewComment: publicProcedure
|
||||
.input(z.object({ id: z.string().min(1) }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const repo = requireReviewCommentRepository(ctx);
|
||||
const comment = await repo.resolve(input.id);
|
||||
if (!comment) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: `Review comment '${input.id}' not found` });
|
||||
}
|
||||
return comment;
|
||||
}),
|
||||
|
||||
unresolveReviewComment: publicProcedure
|
||||
.input(z.object({ id: z.string().min(1) }))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
const repo = requireReviewCommentRepository(ctx);
|
||||
const comment = await repo.unresolve(input.id);
|
||||
if (!comment) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: `Review comment '${input.id}' not found` });
|
||||
}
|
||||
return comment;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useCallback, useMemo, useRef } from "react";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { trpc } from "@/lib/trpc";
|
||||
@@ -6,14 +6,13 @@ import { parseUnifiedDiff } from "./parse-diff";
|
||||
import { DiffViewer } from "./DiffViewer";
|
||||
import { ReviewSidebar } from "./ReviewSidebar";
|
||||
import { ReviewHeader } from "./ReviewHeader";
|
||||
import type { ReviewComment, ReviewStatus, DiffLine } from "./types";
|
||||
import type { ReviewStatus, DiffLine } from "./types";
|
||||
|
||||
interface ReviewTabProps {
|
||||
initiativeId: string;
|
||||
}
|
||||
|
||||
export function ReviewTab({ initiativeId }: ReviewTabProps) {
|
||||
const [comments, setComments] = useState<ReviewComment[]>([]);
|
||||
const [status, setStatus] = useState<ReviewStatus>("pending");
|
||||
const [selectedCommit, setSelectedCommit] = useState<string | null>(null);
|
||||
const fileRefs = useRef<Map<string, HTMLDivElement>>(new Map());
|
||||
@@ -116,6 +115,44 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
|
||||
}
|
||||
: null;
|
||||
|
||||
// Review comments — persisted to DB
|
||||
const utils = trpc.useUtils();
|
||||
const commentsQuery = trpc.listReviewComments.useQuery(
|
||||
{ phaseId: activePhaseId! },
|
||||
{ enabled: !!activePhaseId },
|
||||
);
|
||||
const comments = useMemo(() => {
|
||||
return (commentsQuery.data ?? []).map((c) => ({
|
||||
id: c.id,
|
||||
filePath: c.filePath,
|
||||
lineNumber: c.lineNumber,
|
||||
lineType: c.lineType as "added" | "removed" | "context",
|
||||
body: c.body,
|
||||
author: c.author,
|
||||
createdAt: c.createdAt instanceof Date ? c.createdAt.toISOString() : String(c.createdAt),
|
||||
resolved: c.resolved,
|
||||
}));
|
||||
}, [commentsQuery.data]);
|
||||
|
||||
const createCommentMutation = trpc.createReviewComment.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.listReviewComments.invalidate({ phaseId: activePhaseId! });
|
||||
},
|
||||
onError: (err) => toast.error(`Failed to save comment: ${err.message}`),
|
||||
});
|
||||
|
||||
const resolveCommentMutation = trpc.resolveReviewComment.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.listReviewComments.invalidate({ phaseId: activePhaseId! });
|
||||
},
|
||||
});
|
||||
|
||||
const unresolveCommentMutation = trpc.unresolveReviewComment.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.listReviewComments.invalidate({ phaseId: activePhaseId! });
|
||||
},
|
||||
});
|
||||
|
||||
const approveMutation = trpc.approvePhaseReview.useMutation({
|
||||
onSuccess: () => {
|
||||
setStatus("approved");
|
||||
@@ -141,33 +178,26 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
|
||||
|
||||
const handleAddComment = useCallback(
|
||||
(filePath: string, lineNumber: number, lineType: DiffLine["type"], body: string) => {
|
||||
const newComment: ReviewComment = {
|
||||
id: `c${Date.now()}`,
|
||||
if (!activePhaseId) return;
|
||||
createCommentMutation.mutate({
|
||||
phaseId: activePhaseId,
|
||||
filePath,
|
||||
lineNumber,
|
||||
lineType,
|
||||
body,
|
||||
author: "you",
|
||||
createdAt: new Date().toISOString(),
|
||||
resolved: false,
|
||||
};
|
||||
setComments((prev) => [...prev, newComment]);
|
||||
});
|
||||
toast.success("Comment added");
|
||||
},
|
||||
[],
|
||||
[activePhaseId, createCommentMutation],
|
||||
);
|
||||
|
||||
const handleResolveComment = useCallback((commentId: string) => {
|
||||
setComments((prev) =>
|
||||
prev.map((c) => (c.id === commentId ? { ...c, resolved: true } : c)),
|
||||
);
|
||||
}, []);
|
||||
resolveCommentMutation.mutate({ id: commentId });
|
||||
}, [resolveCommentMutation]);
|
||||
|
||||
const handleUnresolveComment = useCallback((commentId: string) => {
|
||||
setComments((prev) =>
|
||||
prev.map((c) => (c.id === commentId ? { ...c, resolved: false } : c)),
|
||||
);
|
||||
}, []);
|
||||
unresolveCommentMutation.mutate({ id: commentId });
|
||||
}, [unresolveCommentMutation]);
|
||||
|
||||
const handleApprove = useCallback(() => {
|
||||
if (!activePhaseId) return;
|
||||
@@ -192,7 +222,6 @@ export function ReviewTab({ initiativeId }: ReviewTabProps) {
|
||||
setSelectedPhaseId(id);
|
||||
setSelectedCommit(null);
|
||||
setStatus("pending");
|
||||
setComments([]);
|
||||
}, []);
|
||||
|
||||
const unresolvedCount = comments.filter((c) => !c.resolved).length;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
## Architecture
|
||||
|
||||
- **Schema**: `apps/server/db/schema.ts` — all tables, columns, relations
|
||||
- **Ports** (interfaces): `apps/server/db/repositories/*.ts` — 12 repository interfaces
|
||||
- **Adapters** (implementations): `apps/server/db/repositories/drizzle/*.ts` — 12 Drizzle adapters
|
||||
- **Ports** (interfaces): `apps/server/db/repositories/*.ts` — 13 repository interfaces
|
||||
- **Adapters** (implementations): `apps/server/db/repositories/drizzle/*.ts` — 13 Drizzle adapters
|
||||
- **Barrel exports**: `apps/server/db/index.ts` re-exports everything
|
||||
|
||||
All adapters use nanoid() for IDs, auto-manage timestamps, and use Drizzle's `.returning()` for atomic reads after writes.
|
||||
@@ -195,9 +195,27 @@ Messages within a chat session.
|
||||
|
||||
Index: `(chatSessionId)`.
|
||||
|
||||
### review_comments
|
||||
|
||||
Inline review comments on phase diffs, persisted across page reloads.
|
||||
|
||||
| Column | Type | Notes |
|
||||
|--------|------|-------|
|
||||
| id | text PK | nanoid |
|
||||
| phaseId | text FK → phases (cascade) | scopes comment to phase |
|
||||
| filePath | text NOT NULL | file in diff |
|
||||
| lineNumber | integer NOT NULL | line number (new-side or old-side for deletions) |
|
||||
| lineType | text enum | 'added' \| 'removed' \| 'context' |
|
||||
| body | text NOT NULL | comment text |
|
||||
| author | text NOT NULL | default 'you' |
|
||||
| resolved | integer/boolean | default false |
|
||||
| createdAt, updatedAt | integer/timestamp | |
|
||||
|
||||
Index: `(phaseId)`.
|
||||
|
||||
## Repository Interfaces
|
||||
|
||||
12 repositories, each with standard CRUD plus domain-specific methods:
|
||||
13 repositories, each with standard CRUD plus domain-specific methods:
|
||||
|
||||
| Repository | Key Methods |
|
||||
|-----------|-------------|
|
||||
@@ -213,6 +231,7 @@ Index: `(chatSessionId)`.
|
||||
| LogChunkRepository | insertChunk, findByAgentId, deleteByAgentId, getSessionCount |
|
||||
| ConversationRepository | create, findById, findPendingForAgent, answer |
|
||||
| ChatSessionRepository | createSession, findActiveSession, findActiveSessionByAgentId, updateSession, createMessage, findMessagesBySessionId |
|
||||
| ReviewCommentRepository | create, findByPhaseId, resolve, unresolve, delete |
|
||||
|
||||
## Migrations
|
||||
|
||||
@@ -224,4 +243,4 @@ Key rules:
|
||||
- See [database-migrations.md](database-migrations.md) for full workflow
|
||||
- Snapshots stale after 0008; migrations 0008+ are hand-written
|
||||
|
||||
Current migrations: 0000 through 0027 (28 total).
|
||||
Current migrations: 0000 through 0028 (29 total).
|
||||
|
||||
@@ -113,6 +113,10 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE
|
||||
| getPhaseReviewCommits | query | List commits between initiative and phase branch |
|
||||
| getCommitDiff | query | Diff for a single commit (by hash) in a phase |
|
||||
| approvePhaseReview | mutation | Approve and merge phase branch |
|
||||
| listReviewComments | query | List review comments by phaseId |
|
||||
| createReviewComment | mutation | Create inline review comment on diff |
|
||||
| resolveReviewComment | mutation | Mark review comment as resolved |
|
||||
| unresolveReviewComment | mutation | Mark review comment as unresolved |
|
||||
|
||||
### Phase Dispatch
|
||||
| Procedure | Type | Description |
|
||||
|
||||
Reference in New Issue
Block a user