@emiliosp/pi-maestro 0.9.5 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,22 +39,17 @@ const RunVerifierToolParameters = Type.Object(
39
39
 
40
40
  const MILLISECONDS_PER_MINUTE = 60_000;
41
41
 
42
- type CandidateReadyResult = {
43
- outcome: typeof WORKFLOW_PHASES.CANDIDATE_READY;
44
- specId: string;
45
- phase: typeof WORKFLOW_PHASES.CANDIDATE_READY;
46
- handoff: VerifierHandoff;
47
- };
42
+ type VerifierTerminalPhase =
43
+ | typeof WORKFLOW_PHASES.CANDIDATE_READY
44
+ | typeof WORKFLOW_PHASES.FINDING_DECISIONS;
48
45
 
49
- type FindingsResult = {
50
- outcome: typeof WORKFLOW_PHASES.FINDINGS_DECISION;
46
+ type VerifierRunResult = {
47
+ outcome: VerifierTerminalPhase;
51
48
  specId: string;
52
- phase: typeof WORKFLOW_PHASES.FINDINGS_DECISION;
49
+ phase: VerifierTerminalPhase;
53
50
  handoff: VerifierHandoff;
54
51
  };
55
52
 
56
- type VerifierRunResult = CandidateReadyResult | FindingsResult;
57
-
58
53
  type ReadTerminalVerifierResultInput = {
59
54
  paths: MaestroPaths;
60
55
  specId: string;
@@ -65,12 +60,10 @@ const readTerminalVerifierResult = async ({
65
60
  paths,
66
61
  specId,
67
62
  state,
68
- }: ReadTerminalVerifierResultInput): Promise<
69
- CandidateReadyResult | FindingsResult
70
- > => {
63
+ }: ReadTerminalVerifierResultInput): Promise<VerifierRunResult> => {
71
64
  if (
72
65
  state.phase !== WORKFLOW_PHASES.CANDIDATE_READY &&
73
- state.phase !== WORKFLOW_PHASES.FINDINGS_DECISION
66
+ state.phase !== WORKFLOW_PHASES.FINDING_DECISIONS
74
67
  ) {
75
68
  throw new Error(
76
69
  `Verifier returned with unexpected workflow phase "${state.phase}".`,
@@ -84,7 +77,7 @@ const readTerminalVerifierResult = async ({
84
77
 
85
78
  const expectedPhase =
86
79
  handoff.findings.length > 0
87
- ? WORKFLOW_PHASES.FINDINGS_DECISION
80
+ ? WORKFLOW_PHASES.FINDING_DECISIONS
88
81
  : WORKFLOW_PHASES.CANDIDATE_READY;
89
82
 
90
83
  if (state.phase !== expectedPhase) {
@@ -93,16 +86,12 @@ const readTerminalVerifierResult = async ({
93
86
  );
94
87
  }
95
88
 
96
- const details = {
89
+ return {
97
90
  specId,
98
91
  handoff,
92
+ outcome: state.phase,
93
+ phase: state.phase,
99
94
  };
100
-
101
- if (state.phase === WORKFLOW_PHASES.CANDIDATE_READY) {
102
- return { ...details, outcome: state.phase, phase: state.phase };
103
- }
104
-
105
- return { ...details, outcome: state.phase, phase: state.phase };
106
95
  };
107
96
 
108
97
  type ReadVerifierResultInput = {
@@ -17,9 +17,8 @@ export const isPathWithinOrEqual = ({
17
17
  const pathToCandidate = relative(parent, candidate);
18
18
 
19
19
  return (
20
- pathToCandidate === '' ||
21
- (!pathToCandidate.startsWith(`..${sep}`) &&
22
- pathToCandidate !== '..' &&
23
- !isAbsolute(pathToCandidate))
20
+ !pathToCandidate.startsWith(`..${sep}`) &&
21
+ pathToCandidate !== '..' &&
22
+ !isAbsolute(pathToCandidate)
24
23
  );
25
24
  };
@@ -1,17 +1,19 @@
1
1
  /**
2
2
  * Objective: Save all owner resolutions in the active builder handoff.
3
- * Used: When the owner keeps the contract and resolves current questions together.
3
+ * Used: When the owner resolves current questions, with or without spec revision.
4
4
  */
5
5
 
6
6
  import { readBuilderHandoff } from '#artifacts/builder-handoff/readBuilderHandoff.ts';
7
7
  import {
8
8
  BUILDER_HANDOFF_STATUSES,
9
+ type Escalation,
9
10
  type EscalationResolution,
10
11
  } from '#artifacts/builder-handoff/schema.ts';
11
12
  import { writeBuilderHandoff } from '#artifacts/builder-handoff/writeBuilderHandoff.ts';
12
13
  import type { MaestroPaths } from '#MaestroPaths.ts';
14
+ import { getWorkflowEventBySpecChangeRequired } from '#workflow/builder/utils/getWorkflowEventBySpecChangeRequired.ts';
13
15
  import { readWorkflowState } from '#workflow/state/readWorkflowState.ts';
14
- import { WORKFLOW_EVENTS, WORKFLOW_PHASES } from '#workflow/state/schema.ts';
16
+ import { WORKFLOW_PHASES } from '#workflow/state/schema.ts';
15
17
  import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
16
18
  import { transitionWorkflow } from '#workflow/transitions.ts';
17
19
 
@@ -19,43 +21,23 @@ export type EscalationDecision = EscalationResolution & {
19
21
  escalationId: string;
20
22
  };
21
23
 
22
- type ResolveEscalationsInput = {
23
- paths: MaestroPaths;
24
- specId: string;
24
+ type ApplyEscalationDecisionsInput = {
25
+ escalations: Escalation[];
25
26
  decisions: EscalationDecision[];
26
27
  };
27
28
 
28
- export const resolveEscalations = async ({
29
- paths,
30
- specId,
29
+ const applyEscalationDecisions = ({
30
+ escalations,
31
31
  decisions,
32
- }: ResolveEscalationsInput) => {
33
- const workflowPath = paths.getWorkflowPath(specId);
34
- const state = await readWorkflowState(workflowPath);
35
-
36
- if (state.specId !== specId) throw new Error('Workflow spec ID mismatch.');
37
-
38
- if (state.phase !== WORKFLOW_PHASES.ESCALATION_DECISION) {
39
- throw new Error(
40
- `Escalation resolution requires escalation-decision state, found "${state.phase}".`,
41
- );
42
- }
43
-
44
- const handoffPath = await paths.getActiveBuilderHandoffPath(specId);
45
- const handoff = await readBuilderHandoff({ path: handoffPath, specId });
46
-
47
- if (handoff.status !== BUILDER_HANDOFF_STATUSES.ESCALATION) {
48
- throw new Error('Escalation resolution requires an escalation handoff.');
49
- }
50
-
51
- if (handoff.escalations.some(({ resolution }) => resolution !== null)) {
32
+ }: ApplyEscalationDecisionsInput) => {
33
+ if (escalations.some(({ resolution }) => resolution !== null)) {
52
34
  throw new Error('Current questions already contain an owner resolution.');
53
35
  }
54
36
 
55
37
  if (decisions.length === 0)
56
38
  throw new Error('At least one escalation decision is required.');
57
39
 
58
- const expectedIds = new Set(handoff.escalations.map(({ id }) => id));
40
+ const expectedIds = new Set(escalations.map(({ id }) => id));
59
41
  const resolutions = new Map<string, EscalationResolution>();
60
42
 
61
43
  for (const { escalationId, ...resolution } of decisions) {
@@ -68,7 +50,7 @@ export const resolveEscalations = async ({
68
50
  resolutions.set(escalationId, resolution);
69
51
  }
70
52
 
71
- const escalations = handoff.escalations.map((escalation) => {
53
+ return escalations.map((escalation) => {
72
54
  const resolution = resolutions.get(escalation.id);
73
55
 
74
56
  if (resolution === undefined)
@@ -76,12 +58,49 @@ export const resolveEscalations = async ({
76
58
 
77
59
  return { ...escalation, resolution };
78
60
  });
61
+ };
62
+
63
+ type ResolveEscalationsInput = {
64
+ paths: MaestroPaths;
65
+ specId: string;
66
+ specChangeRequired: boolean;
67
+ decisions: EscalationDecision[];
68
+ };
69
+
70
+ export const resolveEscalations = async ({
71
+ paths,
72
+ specId,
73
+ specChangeRequired,
74
+ decisions,
75
+ }: ResolveEscalationsInput) => {
76
+ const workflowPath = paths.getWorkflowPath(specId);
77
+ const state = await readWorkflowState(workflowPath);
78
+
79
+ if (state.specId !== specId) throw new Error('Workflow spec ID mismatch.');
80
+
81
+ if (state.phase !== WORKFLOW_PHASES.ESCALATION_DECISIONS) {
82
+ throw new Error(
83
+ `Escalation resolution requires escalation-decisions state, found "${state.phase}".`,
84
+ );
85
+ }
86
+
87
+ const handoffPath = await paths.getActiveBuilderHandoffPath(specId);
88
+ const handoff = await readBuilderHandoff({ path: handoffPath, specId });
89
+
90
+ if (handoff.status !== BUILDER_HANDOFF_STATUSES.ESCALATION) {
91
+ throw new Error('Escalation resolution requires an escalation handoff.');
92
+ }
93
+
94
+ const escalations = applyEscalationDecisions({
95
+ escalations: handoff.escalations,
96
+ decisions,
97
+ });
79
98
 
80
99
  const nextHandoff = { ...handoff, escalations };
81
100
 
82
101
  const nextState = transitionWorkflow({
83
102
  state,
84
- event: WORKFLOW_EVENTS.RESOLVE_ESCALATION,
103
+ event: getWorkflowEventBySpecChangeRequired(specChangeRequired),
85
104
  });
86
105
 
87
106
  // The artifact writer validates every resolution before the first protocol write.
@@ -17,7 +17,7 @@ export const getWorkflowEventByBuilderHandoff = ({
17
17
  }
18
18
 
19
19
  if (status === BUILDER_HANDOFF_STATUSES.ESCALATION) {
20
- return WORKFLOW_EVENTS.OPEN_ESCALATION;
20
+ return WORKFLOW_EVENTS.BUILDER_OPEN_ESCALATIONS;
21
21
  }
22
22
 
23
23
  throw new Error(`Unexpected builder handoff status: "${status}".`);
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Objective: Select the workflow event for resolving escalations.
3
+ * Used: When owner answers determine whether the spec must change.
4
+ */
5
+
6
+ import { WORKFLOW_EVENTS, type WorkflowEvent } from '#workflow/state/schema.ts';
7
+
8
+ export const getWorkflowEventBySpecChangeRequired = (
9
+ specChangeRequired: boolean,
10
+ ): WorkflowEvent => {
11
+ if (specChangeRequired) {
12
+ return WORKFLOW_EVENTS.OWNER_REQUEST_SPEC_REVISION;
13
+ }
14
+
15
+ return WORKFLOW_EVENTS.RESOLVE_ESCALATIONS;
16
+ };
@@ -13,34 +13,35 @@ export const WORKFLOW_PHASES = {
13
13
  DRAFTING_SPEC: 'drafting-spec',
14
14
  READY_FOR_BUILDER: 'ready-for-builder',
15
15
  BUILDER_RUNNING: 'builder-running',
16
- ESCALATION_DECISION: 'escalation-decision',
16
+ ESCALATION_DECISIONS: 'escalation-decisions',
17
17
  READY_FOR_VERIFIER: 'ready-for-verifier',
18
18
  VERIFIER_RUNNING: 'verifier-running',
19
- FINDINGS_DECISION: 'findings-decision',
19
+ FINDING_DECISIONS: 'finding-decisions',
20
20
  CANDIDATE_READY: 'candidate-ready',
21
21
  } as const;
22
22
 
23
23
  export const WORKFLOW_EVENTS = {
24
24
  MARK_SPEC_READY: 'mark-spec-ready',
25
25
  RUN_BUILDER: 'run-builder',
26
- OPEN_ESCALATION: 'open-escalation',
27
- RESOLVE_ESCALATION: 'resolve-escalation',
26
+ BUILDER_OPEN_ESCALATIONS: 'builder-open-escalations',
27
+ RESOLVE_ESCALATIONS: 'resolve-escalations',
28
+ OWNER_REQUEST_SPEC_REVISION: 'owner-request-spec-revision',
28
29
  BUILDER_DONE: 'builder-done',
29
30
  RUN_VERIFIER: 'run-verifier',
30
31
  VERIFIER_FOUND_FINDINGS: 'verifier-found-findings',
31
- VERIFIER_APPROVED: 'verifier-approved',
32
- REJECT_FINDINGS: 'reject-findings',
33
- REQUEST_FIXES: 'request-fixes',
32
+ VERIFIER_DONE: 'verifier-done',
33
+ OWNER_REJECT_FINDINGS: 'owner-reject-findings',
34
+ OWNER_REQUEST_FIXES: 'owner-request-fixes',
34
35
  } as const;
35
36
 
36
37
  export const WorkflowPhaseSchema = Type.Union([
37
38
  Type.Literal(WORKFLOW_PHASES.DRAFTING_SPEC),
38
39
  Type.Literal(WORKFLOW_PHASES.READY_FOR_BUILDER),
39
40
  Type.Literal(WORKFLOW_PHASES.BUILDER_RUNNING),
40
- Type.Literal(WORKFLOW_PHASES.ESCALATION_DECISION),
41
+ Type.Literal(WORKFLOW_PHASES.ESCALATION_DECISIONS),
41
42
  Type.Literal(WORKFLOW_PHASES.READY_FOR_VERIFIER),
42
43
  Type.Literal(WORKFLOW_PHASES.VERIFIER_RUNNING),
43
- Type.Literal(WORKFLOW_PHASES.FINDINGS_DECISION),
44
+ Type.Literal(WORKFLOW_PHASES.FINDING_DECISIONS),
44
45
  Type.Literal(WORKFLOW_PHASES.CANDIDATE_READY),
45
46
  ]);
46
47
 
@@ -19,25 +19,28 @@ type WorkflowTransition = {
19
19
 
20
20
  export const WORKFLOW_TRANSITIONS: Record<WorkflowEvent, WorkflowTransition> = {
21
21
  [WORKFLOW_EVENTS.MARK_SPEC_READY]: {
22
- from: [
23
- WORKFLOW_PHASES.DRAFTING_SPEC,
24
- WORKFLOW_PHASES.ESCALATION_DECISION,
25
- WORKFLOW_PHASES.FINDINGS_DECISION,
26
- ],
22
+ from: [WORKFLOW_PHASES.DRAFTING_SPEC],
27
23
  to: WORKFLOW_PHASES.READY_FOR_BUILDER,
28
24
  },
29
25
  [WORKFLOW_EVENTS.RUN_BUILDER]: {
30
26
  from: [WORKFLOW_PHASES.READY_FOR_BUILDER],
31
27
  to: WORKFLOW_PHASES.BUILDER_RUNNING,
32
28
  },
33
- [WORKFLOW_EVENTS.OPEN_ESCALATION]: {
29
+ [WORKFLOW_EVENTS.BUILDER_OPEN_ESCALATIONS]: {
34
30
  from: [WORKFLOW_PHASES.BUILDER_RUNNING],
35
- to: WORKFLOW_PHASES.ESCALATION_DECISION,
31
+ to: WORKFLOW_PHASES.ESCALATION_DECISIONS,
36
32
  },
37
- [WORKFLOW_EVENTS.RESOLVE_ESCALATION]: {
38
- from: [WORKFLOW_PHASES.ESCALATION_DECISION],
33
+ [WORKFLOW_EVENTS.RESOLVE_ESCALATIONS]: {
34
+ from: [WORKFLOW_PHASES.ESCALATION_DECISIONS],
39
35
  to: WORKFLOW_PHASES.READY_FOR_BUILDER,
40
36
  },
37
+ [WORKFLOW_EVENTS.OWNER_REQUEST_SPEC_REVISION]: {
38
+ from: [
39
+ WORKFLOW_PHASES.ESCALATION_DECISIONS,
40
+ WORKFLOW_PHASES.FINDING_DECISIONS,
41
+ ],
42
+ to: WORKFLOW_PHASES.DRAFTING_SPEC,
43
+ },
41
44
  [WORKFLOW_EVENTS.BUILDER_DONE]: {
42
45
  from: [WORKFLOW_PHASES.BUILDER_RUNNING],
43
46
  to: WORKFLOW_PHASES.READY_FOR_VERIFIER,
@@ -48,18 +51,18 @@ export const WORKFLOW_TRANSITIONS: Record<WorkflowEvent, WorkflowTransition> = {
48
51
  },
49
52
  [WORKFLOW_EVENTS.VERIFIER_FOUND_FINDINGS]: {
50
53
  from: [WORKFLOW_PHASES.VERIFIER_RUNNING],
51
- to: WORKFLOW_PHASES.FINDINGS_DECISION,
54
+ to: WORKFLOW_PHASES.FINDING_DECISIONS,
52
55
  },
53
- [WORKFLOW_EVENTS.VERIFIER_APPROVED]: {
56
+ [WORKFLOW_EVENTS.VERIFIER_DONE]: {
54
57
  from: [WORKFLOW_PHASES.VERIFIER_RUNNING],
55
58
  to: WORKFLOW_PHASES.CANDIDATE_READY,
56
59
  },
57
- [WORKFLOW_EVENTS.REJECT_FINDINGS]: {
58
- from: [WORKFLOW_PHASES.FINDINGS_DECISION],
60
+ [WORKFLOW_EVENTS.OWNER_REJECT_FINDINGS]: {
61
+ from: [WORKFLOW_PHASES.FINDING_DECISIONS],
59
62
  to: WORKFLOW_PHASES.CANDIDATE_READY,
60
63
  },
61
- [WORKFLOW_EVENTS.REQUEST_FIXES]: {
62
- from: [WORKFLOW_PHASES.FINDINGS_DECISION],
64
+ [WORKFLOW_EVENTS.OWNER_REQUEST_FIXES]: {
65
+ from: [WORKFLOW_PHASES.FINDING_DECISIONS],
63
66
  to: WORKFLOW_PHASES.READY_FOR_BUILDER,
64
67
  },
65
68
  };
@@ -74,22 +77,21 @@ export const transitionWorkflow = ({
74
77
  event,
75
78
  }: TransitionWorkflowInput): WorkflowState => {
76
79
  assertWorkflowState(state);
77
- const current = state;
78
80
  const transition = WORKFLOW_TRANSITIONS[event];
79
81
 
80
82
  if (transition === undefined) {
81
83
  throw new Error(`Unknown workflow event: "${event}".`);
82
84
  }
83
85
 
84
- if (!transition.from.includes(current.phase)) {
86
+ if (!transition.from.includes(state.phase)) {
85
87
  throw new Error(
86
- `Workflow event "${event}" is not allowed from phase "${current.phase}".`,
88
+ `Workflow event "${event}" is not allowed from phase "${state.phase}".`,
87
89
  );
88
90
  }
89
91
 
90
92
  return {
91
- version: current.version,
92
- specId: current.specId,
93
+ version: state.version,
94
+ specId: state.specId,
93
95
  phase: transition.to,
94
96
  };
95
97
  };
@@ -9,13 +9,13 @@ import { WORKFLOW_PHASES, type WorkflowState } from '#workflow/state/schema.ts';
9
9
 
10
10
  const BUILDER_PHASES: readonly WorkflowState['phase'][] = [
11
11
  WORKFLOW_PHASES.BUILDER_RUNNING,
12
- WORKFLOW_PHASES.ESCALATION_DECISION,
12
+ WORKFLOW_PHASES.ESCALATION_DECISIONS,
13
13
  WORKFLOW_PHASES.READY_FOR_VERIFIER,
14
14
  ];
15
15
 
16
16
  const VERIFIER_PHASES: readonly WorkflowState['phase'][] = [
17
17
  WORKFLOW_PHASES.VERIFIER_RUNNING,
18
- WORKFLOW_PHASES.FINDINGS_DECISION,
18
+ WORKFLOW_PHASES.FINDING_DECISIONS,
19
19
  ];
20
20
 
21
21
  export const getAgentRole = (
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Objective: Record owner decisions for current verifier findings on the active handoff.
3
- * Used: When the owner resolves a findings-decision handoff.
3
+ * Used: When the owner resolves a finding-decisions handoff.
4
4
  */
5
5
 
6
6
  import { assertVerifierHandoff } from '#artifacts/verifier-handoff/assertVerifierHandoff.ts';
@@ -18,46 +18,42 @@ import { writeWorkflowState } from '#workflow/state/writeWorkflowState.ts';
18
18
  import { transitionWorkflow } from '#workflow/transitions.ts';
19
19
  import { getWorkflowEventByVerifierHandoff } from '#workflow/verifier/utils/getWorkflowEventByVerifierHandoff.ts';
20
20
 
21
- type FindingDecisionRejected = {
22
- findingId: string;
23
- decision: typeof FINDING_DECISIONS.REJECT;
24
- reason: string;
25
- };
21
+ type SavedFindingDecision = NonNullable<VerifierFinding['decision']>;
26
22
 
27
- type FindingDecisionApproved = {
23
+ export type FindingDecision = SavedFindingDecision & {
28
24
  findingId: string;
29
- decision: typeof FINDING_DECISIONS.FIX_CODE;
30
25
  };
31
26
 
32
- export type FindingDecision = FindingDecisionApproved | FindingDecisionRejected;
33
-
34
27
  export type ResolvedFindings = {
35
28
  findings: VerifierFinding[];
36
29
  state: WorkflowState;
37
30
  projectRoot: string;
38
31
  };
39
32
 
40
- type AssertDecisionsInput = {
33
+ type ApplyFindingDecisionsInput = {
41
34
  decisions: FindingDecision[];
42
35
  findings: VerifierFinding[];
43
36
  };
44
37
 
45
- function assertDecisions({ decisions, findings }: AssertDecisionsInput): void {
38
+ const applyFindingDecisions = ({
39
+ decisions,
40
+ findings,
41
+ }: ApplyFindingDecisionsInput): VerifierFinding[] => {
46
42
  const expectedIds = new Set(findings.map(({ id }) => id));
47
- const seenIds = new Set<string>();
43
+ const decisionsById = new Map<string, FindingDecision>();
48
44
 
49
45
  for (const decision of decisions) {
50
46
  if (!expectedIds.has(decision.findingId)) {
51
47
  throw new Error(`Unknown finding ID: "${decision.findingId}".`);
52
48
  }
53
49
 
54
- if (seenIds.has(decision.findingId)) {
50
+ if (decisionsById.has(decision.findingId)) {
55
51
  throw new Error(
56
52
  `Duplicate decision for finding "${decision.findingId}".`,
57
53
  );
58
54
  }
59
55
 
60
- seenIds.add(decision.findingId);
56
+ decisionsById.set(decision.findingId, decision);
61
57
 
62
58
  if (
63
59
  decision.decision === FINDING_DECISIONS.REJECT &&
@@ -69,12 +65,17 @@ function assertDecisions({ decisions, findings }: AssertDecisionsInput): void {
69
65
  }
70
66
  }
71
67
 
72
- const missing = findings.find(({ id }) => !seenIds.has(id));
68
+ const missing = findings.find(({ id }) => !decisionsById.has(id));
73
69
 
74
70
  if (missing !== undefined) {
75
71
  throw new Error(`Missing decision for finding "${missing.id}".`);
76
72
  }
77
- }
73
+
74
+ return findings.map((finding) => ({
75
+ ...finding,
76
+ decision: getSavedFindingDecision(decisionsById.get(finding.id)),
77
+ }));
78
+ };
78
79
 
79
80
  type FindingWorkflow = {
80
81
  state: WorkflowState;
@@ -98,9 +99,9 @@ const readFindingWorkflow = async ({
98
99
  );
99
100
  }
100
101
 
101
- if (state.phase !== WORKFLOW_PHASES.FINDINGS_DECISION) {
102
+ if (state.phase !== WORKFLOW_PHASES.FINDING_DECISIONS) {
102
103
  throw new Error(
103
- `Finding resolution requires findings-decision state, found "${state.phase}".`,
104
+ `Finding resolution requires finding-decisions state, found "${state.phase}".`,
104
105
  );
105
106
  }
106
107
 
@@ -124,51 +125,25 @@ const readFindingWorkflow = async ({
124
125
  return { state, handoff };
125
126
  };
126
127
 
127
- type BuildFindingWorkflowInput = {
128
- workflow: FindingWorkflow;
129
- decisions: FindingDecision[];
130
- };
131
-
132
- const buildFindingWorkflow = ({
133
- workflow,
134
- decisions,
135
- }: BuildFindingWorkflowInput): FindingWorkflow => {
136
- const { state, handoff } = workflow;
137
- assertDecisions({ decisions, findings: handoff.findings });
138
-
139
- const decisionsById = new Map(
140
- decisions.map((decision) => [decision.findingId, decision]),
141
- );
142
-
143
- const findings = handoff.findings.map((finding) => {
144
- const decision = decisionsById.get(finding.id);
145
-
146
- if (decision?.decision === FINDING_DECISIONS.REJECT) {
147
- return {
148
- ...finding,
149
- decision: {
150
- decision: FINDING_DECISIONS.REJECT,
151
- reason: decision.reason.trim(),
152
- },
153
- };
154
- }
155
-
156
- return { ...finding, decision: { decision: FINDING_DECISIONS.FIX_CODE } };
157
- });
158
-
159
- const nextHandoff = { ...handoff, findings };
160
-
161
- const nextState = transitionWorkflow({
162
- state,
163
- event: getWorkflowEventByVerifierHandoff(nextHandoff),
164
- });
128
+ const getSavedFindingDecision = (
129
+ decision: FindingDecision | undefined,
130
+ ): SavedFindingDecision => {
131
+ if (decision?.decision === FINDING_DECISIONS.REVISE_SPEC) {
132
+ return {
133
+ decision: FINDING_DECISIONS.REVISE_SPEC,
134
+ reason: decision.reason.trim(),
135
+ specChange: decision.specChange,
136
+ };
137
+ }
165
138
 
166
- assertVerifierHandoff({
167
- handoff: nextHandoff,
168
- specId: state.specId,
169
- });
139
+ if (decision?.decision === FINDING_DECISIONS.REJECT) {
140
+ return {
141
+ decision: FINDING_DECISIONS.REJECT,
142
+ reason: decision.reason.trim(),
143
+ };
144
+ }
170
145
 
171
- return { state: nextState, handoff: nextHandoff };
146
+ return { decision: FINDING_DECISIONS.FIX_CODE };
172
147
  };
173
148
 
174
149
  type ResolveFindingsInput = {
@@ -182,23 +157,35 @@ export const resolveFindings = async ({
182
157
  specId,
183
158
  decisions,
184
159
  }: ResolveFindingsInput): Promise<ResolvedFindings> => {
185
- const currentWorkflow = await readFindingWorkflow({ paths, specId });
160
+ const { state, handoff } = await readFindingWorkflow({ paths, specId });
186
161
 
187
- const workflow = buildFindingWorkflow({
188
- workflow: currentWorkflow,
162
+ const findings = applyFindingDecisions({
189
163
  decisions,
164
+ findings: handoff.findings,
165
+ });
166
+
167
+ const nextHandoff = { ...handoff, findings };
168
+
169
+ const nextState = transitionWorkflow({
170
+ state,
171
+ event: getWorkflowEventByVerifierHandoff(nextHandoff),
172
+ });
173
+
174
+ assertVerifierHandoff({
175
+ handoff: nextHandoff,
176
+ specId: state.specId,
190
177
  });
191
178
 
192
179
  const handoffPath = await paths.getActiveVerifierHandoffPath(specId);
193
- await writeJson({ path: handoffPath, data: workflow.handoff });
180
+ await writeJson({ path: handoffPath, data: nextHandoff });
194
181
  await writeWorkflowState({
195
182
  path: paths.getWorkflowPath(specId),
196
- state: workflow.state,
183
+ state: nextState,
197
184
  });
198
185
 
199
186
  return {
200
- findings: workflow.handoff.findings,
201
- state: workflow.state,
187
+ findings,
188
+ state: nextState,
202
189
  projectRoot: paths.getProjectRoot(),
203
190
  };
204
191
  };
@@ -13,19 +13,27 @@ export const getWorkflowEventByVerifierHandoff = (
13
13
  handoff: VerifierHandoff,
14
14
  ): WorkflowEvent => {
15
15
  if (handoff.findings.length === 0) {
16
- return WORKFLOW_EVENTS.VERIFIER_APPROVED;
16
+ return WORKFLOW_EVENTS.VERIFIER_DONE;
17
17
  }
18
18
 
19
19
  if (handoff.findings.every(({ decision }) => decision === null)) {
20
20
  return WORKFLOW_EVENTS.VERIFIER_FOUND_FINDINGS;
21
21
  }
22
22
 
23
+ if (
24
+ handoff.findings.some(
25
+ ({ decision }) => decision?.decision === FINDING_DECISIONS.REVISE_SPEC,
26
+ )
27
+ ) {
28
+ return WORKFLOW_EVENTS.OWNER_REQUEST_SPEC_REVISION;
29
+ }
30
+
23
31
  if (
24
32
  handoff.findings.some(
25
33
  ({ decision }) => decision?.decision === FINDING_DECISIONS.FIX_CODE,
26
34
  )
27
35
  ) {
28
- return WORKFLOW_EVENTS.REQUEST_FIXES;
36
+ return WORKFLOW_EVENTS.OWNER_REQUEST_FIXES;
29
37
  }
30
38
 
31
39
  if (
@@ -33,7 +41,7 @@ export const getWorkflowEventByVerifierHandoff = (
33
41
  ({ decision }) => decision?.decision === FINDING_DECISIONS.REJECT,
34
42
  )
35
43
  ) {
36
- return WORKFLOW_EVENTS.REJECT_FINDINGS;
44
+ return WORKFLOW_EVENTS.OWNER_REJECT_FINDINGS;
37
45
  }
38
46
 
39
47
  throw new Error('Unexpected verifier finding decisions.');