# All files / src/trpc/routers change-set.ts

1.51% Statements 1/66
0% Branches 0/53
25% Functions 1/4
1.58% Lines 1/63

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147

    11x

     /**
     * 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 };
          }),
      };
    }