Maybe, since you appear to have a lot of common descriptions between tasks, taskgroups, and processes...
Code:
Lines: LineID*, LineDescr, any other line specific field(s)
ProcessesInLines: LineID*(fk), ProcessID*(fk), ProcessOrder
Processes: ProcessID*, ProcessDescr, other process specific field(s)
TaskGroupsInProcesses: ProcessID*(fk), TaskGroupID*(fk), TaskGroupOrder
TaskGroups:TaskGroupID*, TaskGroupDescr, other task group specific field(s)
TasksInTaskGroups:TaskGroupID*(fk), TaskID*(fk), TaskOrder
Tasks: TaskID, TaskDescr, other task specific field(s)
Results:LineID*(fk), ProcessID*(fk), TaskGroupID*(fk), TaskID*(fk), PassFail, PassFailQty
example results data:
2,4,2,3,'F',10 (10 units failed Line 2, Process 4, Task Group 2, Task 3)
2,4,2,3,'P',321 (321 units passed Line 2, Process 4, Task Group 2, Task 3)
If you add a surrogate ID field to each of the intersection tables (XinY), this table could become:
Code:
Results: ProcessesInLineID*(fk), TaskGroupInProcessID*(fk), TaskInTaskGroupID*(fk), PassFail, PassFailQty
The approaches above would force you to have a record status with qty for each task, but, you can more easily use aggregate functions against the single table. And, you can easily sum pass and/or fail records at any level you need.
Since the records in all the tables except the results table will change rarely, additional surrogate keys and indexes should help, not hurt performance.
* indicates primary key field, (fk) indicates foreign key relationship