Files
Codewalkers/apps/server/trpc/routers/change-set.ts
Lukas May 34578d39c6 refactor: Restructure monorepo to apps/server/ and apps/web/ layout
Move src/ → apps/server/ and packages/web/ → apps/web/ to adopt
standard monorepo conventions (apps/ for runnable apps, packages/
for reusable libraries). Update all config files, shared package
imports, test fixtures, and documentation to reflect new paths.

Key fixes:
- Update workspace config to ["apps/*", "packages/*"]
- Update tsconfig.json rootDir/include for apps/server/
- Add apps/web/** to vitest exclude list
- Update drizzle.config.ts schema path
- Fix ensure-schema.ts migration path detection (3 levels up in dev,
  2 levels up in dist)
- Fix tests/integration/cli-server.test.ts import paths
- Update packages/shared imports to apps/server/ paths
- Update all docs/ files with new paths
2026-03-03 11:22:53 +01:00

147 lines
5.4 KiB
TypeScript

/**
* Change Set Router — list, get, revert workflows
*/
import { TRPCError } from '@trpc/server';
import { z } from 'zod';
import type { ProcedureBuilder } from '../trpc.js';
import {
requireChangeSetRepository,
requirePhaseRepository,
requireTaskRepository,
requirePageRepository,
} from './_helpers.js';
export function changeSetProcedures(publicProcedure: ProcedureBuilder) {
return {
listChangeSets: publicProcedure
.input(z.object({
initiativeId: z.string().min(1).optional(),
agentId: z.string().min(1).optional(),
}))
.query(async ({ ctx, input }) => {
const repo = requireChangeSetRepository(ctx);
if (input.agentId) {
return repo.findByAgentId(input.agentId);
}
if (input.initiativeId) {
return repo.findByInitiativeId(input.initiativeId);
}
throw new TRPCError({
code: 'BAD_REQUEST',
message: 'Either agentId or initiativeId is required',
});
}),
getChangeSet: publicProcedure
.input(z.object({ id: z.string().min(1) }))
.query(async ({ ctx, input }) => {
const repo = requireChangeSetRepository(ctx);
const cs = await repo.findByIdWithEntries(input.id);
if (!cs) {
throw new TRPCError({ code: 'NOT_FOUND', message: `ChangeSet '${input.id}' not found` });
}
return cs;
}),
revertChangeSet: publicProcedure
.input(z.object({ id: z.string().min(1), force: z.boolean().optional() }))
.mutation(async ({ ctx, input }) => {
const repo = requireChangeSetRepository(ctx);
const cs = await repo.findByIdWithEntries(input.id);
if (!cs) {
throw new TRPCError({ code: 'NOT_FOUND', message: `ChangeSet '${input.id}' not found` });
}
if (cs.status === 'reverted') {
throw new TRPCError({ code: 'BAD_REQUEST', message: 'ChangeSet is already reverted' });
}
const phaseRepo = requirePhaseRepository(ctx);
const taskRepo = requireTaskRepository(ctx);
const pageRepo = requirePageRepository(ctx);
// Conflict detection (unless force)
if (!input.force) {
const conflicts: string[] = [];
for (const entry of cs.entries) {
if (entry.action === 'create') {
if (entry.entityType === 'phase') {
const phase = await phaseRepo.findById(entry.entityId);
if (phase && phase.status === 'in_progress') {
conflicts.push(`Phase "${phase.name}" is in progress`);
}
} else if (entry.entityType === 'task') {
const task = await taskRepo.findById(entry.entityId);
if (task && task.status === 'in_progress') {
conflicts.push(`Task "${task.name}" is in progress`);
}
}
} else if (entry.action === 'update' && entry.entityType === 'page' && entry.newState) {
const page = await pageRepo.findById(entry.entityId);
if (page) {
const expectedContent = JSON.parse(entry.newState).content;
if (page.content !== expectedContent) {
conflicts.push(`Page "${page.title}" was modified since change set was applied`);
}
}
}
}
if (conflicts.length > 0) {
return { success: false as const, conflicts };
}
}
// Apply reverts in reverse entry order
const reversedEntries = [...cs.entries].reverse();
for (const entry of reversedEntries) {
try {
if (entry.action === 'create') {
switch (entry.entityType) {
case 'phase':
try { await phaseRepo.delete(entry.entityId); } catch { /* already deleted */ }
break;
case 'task':
try { await taskRepo.delete(entry.entityId); } catch { /* already deleted */ }
break;
case 'phase_dependency': {
const depData = JSON.parse(entry.newState || '{}');
if (depData.phaseId && depData.dependsOnPhaseId) {
try { await phaseRepo.removeDependency(depData.phaseId, depData.dependsOnPhaseId); } catch { /* already removed */ }
}
break;
}
}
} else if (entry.action === 'update' && entry.previousState) {
const prev = JSON.parse(entry.previousState);
switch (entry.entityType) {
case 'page':
await pageRepo.update(entry.entityId, {
content: prev.content,
title: prev.title,
});
ctx.eventBus.emit({
type: 'page:updated',
timestamp: new Date(),
payload: { pageId: entry.entityId, initiativeId: cs.initiativeId, title: prev.title },
});
break;
}
}
} catch (err) {
// Log but continue reverting other entries
}
}
await repo.markReverted(input.id);
ctx.eventBus.emit({
type: 'changeset:reverted' as const,
timestamp: new Date(),
payload: { changeSetId: cs.id, initiativeId: cs.initiativeId },
});
return { success: true as const };
}),
};
}