<?xml version="1.0"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">
	<channel>
		<title>Expertiza_Wiki  - Recent changes [en]</title>
		<link>https://wiki.expertiza.ncsu.edu/index.php?title=Special:RecentChanges</link>
		<description>Track the most recent changes to the wiki in this feed.</description>
		<language>en</language>
		<generator>MediaWiki 1.41.0</generator>
		<lastBuildDate>Fri, 08 May 2026 01:26:06 GMT</lastBuildDate>
		<item>
			<title>CSC/ECE 517 Spring 2026 - E2617. Testing Questionnaire and Course Models</title>
			<link>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168201&amp;oldid=168167</link>
			<guid isPermaLink="false">https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;diff=168201&amp;oldid=168167</guid>
			<description>&lt;p&gt;&lt;/p&gt;
&lt;a href=&quot;https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models&amp;amp;diff=168201&amp;amp;oldid=168167&quot;&gt;Show changes&lt;/a&gt;</description>
			<pubDate>Mon, 04 May 2026 19:42:08 GMT</pubDate>
			<dc:creator>Zobrook2</dc:creator>
			<comments>https://wiki.expertiza.ncsu.edu/index.php?title=Talk:CSC/ECE_517_Spring_2026_-_E2617._Testing_Questionnaire_and_Course_Models</comments>
		</item>
		<item>
			<title>CSC/ECE 517 Spring 2026 - E2605. Specialized Rubrics for Different Topic Types</title>
			<link>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2605._Specialized_Rubrics_for_Different_Topic_Types&amp;diff=168200&amp;oldid=168173</link>
			<guid isPermaLink="false">https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2605._Specialized_Rubrics_for_Different_Topic_Types&amp;diff=168200&amp;oldid=168173</guid>
			<description>&lt;p&gt;&lt;span dir=&quot;auto&quot;&gt;&lt;span class=&quot;autocomment&quot;&gt;Links&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #fff; color: #202122;&quot; data-mw=&quot;interface&quot;&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;tr class=&quot;diff-title&quot; lang=&quot;en&quot;&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;← Older revision&lt;/td&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;Revision as of 01:40, 3 May 2026&lt;/td&gt;
				&lt;/tr&gt;&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l737&quot;&gt;Line 737:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 737:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;Demo Video:&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;Demo Video:&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* &lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Add demo video link here&lt;/del&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* &lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;https://youtu.be/TRxBHXo7GQQ&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;== References ==&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;== References ==&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;</description>
			<pubDate>Sun, 03 May 2026 01:40:01 GMT</pubDate>
			<dc:creator>Vagarwa3</dc:creator>
			<comments>https://wiki.expertiza.ncsu.edu/index.php?title=Talk:CSC/ECE_517_Spring_2026_-_E2605._Specialized_Rubrics_for_Different_Topic_Types</comments>
		</item>
		<item>
			<title>Report Generation Framework (WIP)</title>
			<link>https://wiki.expertiza.ncsu.edu/index.php?title=Report_Generation_Framework_(WIP)&amp;diff=168199&amp;oldid=0</link>
			<guid isPermaLink="false">https://wiki.expertiza.ncsu.edu/index.php?title=Report_Generation_Framework_(WIP)&amp;diff=168199&amp;oldid=0</guid>
			<description>&lt;p&gt;Created page with &amp;quot;= Report Generation = == Design and Implementation Documentation == === Expertiza Reimplementation Back-End ===  ----  == Overview ==  The reporting subsystem was ported from the original Expertiza codebase (referred to as &amp;#039;&amp;#039;&amp;#039;Repo X&amp;#039;&amp;#039;&amp;#039;) into the reimplemented Rails API back-end (referred to as &amp;#039;&amp;#039;&amp;#039;Repo Y&amp;#039;&amp;#039;&amp;#039;) and redesigned in the process.  Repo X used a Rails helper module (&amp;lt;code&amp;gt;ReportFormatterHelper&amp;lt;/code&amp;gt;) that assigned instance variables, such as &amp;lt;code&amp;gt;@reviewers&amp;lt;/cod...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;= Report Generation =&lt;br /&gt;
== Design and Implementation Documentation ==&lt;br /&gt;
=== Expertiza Reimplementation Back-End ===&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
The reporting subsystem was ported from the original Expertiza codebase&lt;br /&gt;
(referred to as '''Repo X''') into the reimplemented Rails API back-end&lt;br /&gt;
(referred to as '''Repo Y''') and redesigned in the process.&lt;br /&gt;
&lt;br /&gt;
Repo X used a Rails helper module (&amp;lt;code&amp;gt;ReportFormatterHelper&amp;lt;/code&amp;gt;) that&lt;br /&gt;
assigned instance variables, such as &amp;lt;code&amp;gt;@reviewers&amp;lt;/code&amp;gt; and&lt;br /&gt;
&amp;lt;code&amp;gt;@review_scores&amp;lt;/code&amp;gt;, for ERB views.&lt;br /&gt;
&lt;br /&gt;
Since Repo Y is a JSON API with no views, instance variables are not applicable.&lt;br /&gt;
The architecture was therefore redesigned around a '''streaming reduce pipeline'''&lt;br /&gt;
that returns plain Ruby hashes rendered as JSON.&lt;br /&gt;
&lt;br /&gt;
=== Design Goals ===&lt;br /&gt;
&lt;br /&gt;
* Avoid loading entire result sets into memory at once.&lt;br /&gt;
* Keep domain-specific computation out of the base class.&lt;br /&gt;
* Make each report type independently composable and testable.&lt;br /&gt;
* Fix N+1 query patterns present in Repo X.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Anti-Patterns Addressed ==&lt;br /&gt;
&lt;br /&gt;
The design directly addresses two anti-patterns identified during architectural&lt;br /&gt;
review of Repo X.&lt;br /&gt;
&lt;br /&gt;
=== The &amp;lt;code&amp;gt;fetch_responses&amp;lt;/code&amp;gt; Anti-Pattern ===&lt;br /&gt;
&lt;br /&gt;
Repo X loaded all response records into an unnamed, ad-hoc array before&lt;br /&gt;
processing:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# WRONG&lt;br /&gt;
responses = fetch_responses   # full memory load&lt;br /&gt;
grouped   = group(responses)&lt;br /&gt;
metrics   = compute_metrics(grouped)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This forces the entire result set into Ruby memory, prevents streaming, and&lt;br /&gt;
makes the intermediate structure implicit.&lt;br /&gt;
&lt;br /&gt;
The fix is to '''never materialise all rows at once'''. Instead, use&lt;br /&gt;
&amp;lt;code&amp;gt;find_each&amp;lt;/code&amp;gt; to stream records in batches, so memory usage scales&lt;br /&gt;
with the number of '''groups''', not the number of raw rows.&lt;br /&gt;
&lt;br /&gt;
=== Default Metrics in the Base Class ===&lt;br /&gt;
&lt;br /&gt;
Repo X placed domain-specific math directly in the base class:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# WRONG — in BaseReport&lt;br /&gt;
def compute_metrics(grouped)&lt;br /&gt;
  grouped.transform_values do |responses|&lt;br /&gt;
    {&lt;br /&gt;
      count:     responses.size,&lt;br /&gt;
      avg_score: responses.map(&amp;amp;:score).sum / responses.size.to_f&lt;br /&gt;
    }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This ties every subclass to one particular shape of computation. The fix is&lt;br /&gt;
to move all domain math into per-report accumulator logic. The base class&lt;br /&gt;
contains '''zero''' domain math.&lt;br /&gt;
&lt;br /&gt;
==== What &amp;quot;Domain Math&amp;quot; Means ====&lt;br /&gt;
&lt;br /&gt;
'''Domain math''' refers to the business-logic calculations specific to a&lt;br /&gt;
particular report type — the actual formulas and aggregations that answer what&lt;br /&gt;
the report is trying to show.&lt;br /&gt;
&lt;br /&gt;
It is called &amp;quot;domain&amp;quot; math because it belongs to the problem domain&lt;br /&gt;
(peer assessment), not to the generic pipeline machinery.&lt;br /&gt;
&lt;br /&gt;
Each report type has its own domain math:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Report !! Its domain math&lt;br /&gt;
|-&lt;br /&gt;
| Review scores&lt;br /&gt;
| &amp;lt;code&amp;gt;(raw_score / max_score) * 100&amp;lt;/code&amp;gt; — percentage score per reviewer per round&lt;br /&gt;
|-&lt;br /&gt;
| Avg/ranges&lt;br /&gt;
| &amp;lt;code&amp;gt;max&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;min&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;sum / size&amp;lt;/code&amp;gt; — score aggregates across a team's reviewers&lt;br /&gt;
|-&lt;br /&gt;
| Feedback&lt;br /&gt;
| Bucket response IDs into &amp;lt;code&amp;gt;round_1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;round_2&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;round_3&amp;lt;/code&amp;gt; arrays&lt;br /&gt;
|-&lt;br /&gt;
| Bookmark rating&lt;br /&gt;
| Collect distinct bookmark IDs into a &amp;lt;code&amp;gt;Set&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Notice that these are completely different in shape: one computes a percentage,&lt;br /&gt;
another computes min/max/avg, and another just collects IDs.&lt;br /&gt;
&lt;br /&gt;
If &amp;lt;code&amp;gt;avg_score&amp;lt;/code&amp;gt; lived in &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt;, every subclass&lt;br /&gt;
would either inherit math it does not need — for example, a bookmark report has&lt;br /&gt;
no scores — or be forced to override the method just to suppress it.&lt;br /&gt;
&lt;br /&gt;
The rule is therefore:&lt;br /&gt;
&lt;br /&gt;
&amp;gt; &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt; only defines '''how to run the pipeline''' — stream, group, fold, finalize.  &lt;br /&gt;
&amp;gt; The '''what to compute''' belongs entirely inside each report's own &amp;lt;code&amp;gt;accumulate&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;finalize&amp;lt;/code&amp;gt; methods.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== System Diagrams [To be updated] ==&lt;br /&gt;
&lt;br /&gt;
=== Overall Request Flow ===&lt;br /&gt;
&lt;br /&gt;
The diagram below shows how an incoming HTTP request travels through the&lt;br /&gt;
system from the controller down to the pipeline and back out as JSON.&lt;br /&gt;
&lt;br /&gt;
Since MediaWiki does not render TikZ directly, this diagram is represented&lt;br /&gt;
as a text-based flow.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
HTTP Client (Front-end)&lt;br /&gt;
        |&lt;br /&gt;
        | GET /reports/response_report?assignment_id=&amp;amp;type=&lt;br /&gt;
        v&lt;br /&gt;
ReportsController#response_report&lt;br /&gt;
        |&lt;br /&gt;
        | params[:type]&lt;br /&gt;
        v&lt;br /&gt;
REPORT_CLASSES[type]&lt;br /&gt;
look up concrete class&lt;br /&gt;
        |&lt;br /&gt;
        | .new(assignment).run&lt;br /&gt;
        v&lt;br /&gt;
Concrete Report&lt;br /&gt;
for example, FeedbackReport&lt;br /&gt;
        |&lt;br /&gt;
        | inherits run&lt;br /&gt;
        v&lt;br /&gt;
BaseReport#run&lt;br /&gt;
inherited find_each streaming loop&lt;br /&gt;
        |&lt;br /&gt;
        | calls subclass methods&lt;br /&gt;
        v&lt;br /&gt;
source -&amp;gt; grouper -&amp;gt; accumulate -&amp;gt; finalize&lt;br /&gt;
        |&lt;br /&gt;
        | output hash&lt;br /&gt;
        v&lt;br /&gt;
render json: { ... }&lt;br /&gt;
        |&lt;br /&gt;
        | JSON response&lt;br /&gt;
        v&lt;br /&gt;
HTTP Client (Front-end)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
''Figure: End-to-end request flow for report generation''&lt;br /&gt;
&lt;br /&gt;
=== Pipeline Internals ===&lt;br /&gt;
&lt;br /&gt;
This diagram shows the four stages inside &amp;lt;code&amp;gt;BaseReport#run&amp;lt;/code&amp;gt;.&lt;br /&gt;
The stages are defined by each concrete subclass; the pipeline loop itself&lt;br /&gt;
never changes.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
+------------------+        +------------------+&lt;br /&gt;
| 1. Source        | rows   | 2. Grouper       |&lt;br /&gt;
| AR relation      | -----&amp;gt; | lambda: row-&amp;gt;key |&lt;br /&gt;
| streamed via     |        | e.g. reviewer_id |&lt;br /&gt;
| find_each        |        +------------------+&lt;br /&gt;
+------------------+                 |&lt;br /&gt;
                                     | key, row&lt;br /&gt;
                                     v&lt;br /&gt;
+------------------+        +------------------+&lt;br /&gt;
| 4. Finalize      | state  | 3. Accumulate    |&lt;br /&gt;
| shape state into | &amp;lt;----- | fold row into    |&lt;br /&gt;
| output hash      |        | state            |&lt;br /&gt;
+------------------+        | domain math here |&lt;br /&gt;
        |                   +------------------+&lt;br /&gt;
        |&lt;br /&gt;
        v&lt;br /&gt;
Hash -&amp;gt; JSON&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Additional notes:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;source&amp;lt;/code&amp;gt; uses &amp;lt;code&amp;gt;includes(...)&amp;lt;/code&amp;gt; where needed to avoid N+1 queries.&lt;br /&gt;
* &amp;lt;code&amp;gt;accumulate&amp;lt;/code&amp;gt; handles scores, deduplication, bucketing, and counting depending on the report type.&lt;br /&gt;
&lt;br /&gt;
''Figure: The four stages every report passes through inside BaseReport#run''&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy ===&lt;br /&gt;
&lt;br /&gt;
This diagram shows how concrete report classes relate to &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Solid inheritance from &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt; is represented by indentation.&lt;br /&gt;
Composition from &amp;lt;code&amp;gt;ReviewReport&amp;lt;/code&amp;gt; to its inner pipelines is represented&lt;br /&gt;
under the coordinator.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
BaseReport&lt;br /&gt;
|&lt;br /&gt;
|-- ReviewReport (coordinator)&lt;br /&gt;
|     |&lt;br /&gt;
|     |-- ReviewersPipeline&lt;br /&gt;
|     |-- ScoresPipeline&lt;br /&gt;
|     |-- AvgRangesPipeline&lt;br /&gt;
|&lt;br /&gt;
|-- FeedbackReport&lt;br /&gt;
|-- TeammateReviewReport&lt;br /&gt;
|-- BookmarkRatingReport&lt;br /&gt;
|-- BasicReport&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Legend:&lt;br /&gt;
&lt;br /&gt;
* Direct child under &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt; = inherits &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt;&lt;br /&gt;
* Pipelines under &amp;lt;code&amp;gt;ReviewReport&amp;lt;/code&amp;gt; = coordinator runs inner pipeline&lt;br /&gt;
* The inner pipelines also inherit &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
''Figure: Class hierarchy for the report generation subsystem''&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Architecture: The Streaming Pipeline ==&lt;br /&gt;
&lt;br /&gt;
=== Pipeline Shape ===&lt;br /&gt;
&lt;br /&gt;
All reports are built on a single pipeline template defined in&lt;br /&gt;
&amp;lt;code&amp;gt;Reports::BaseReport&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def run&lt;br /&gt;
  state = initial_state&lt;br /&gt;
  source.find_each(batch_size: 500) do |row|&lt;br /&gt;
    accumulate(state, grouper.call(row), row)&lt;br /&gt;
  end&lt;br /&gt;
  finalize(state)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The pipeline consists of four concerns:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Concern !! Responsibility&lt;br /&gt;
|-&lt;br /&gt;
| '''Source'''&lt;br /&gt;
| ActiveRecord relation streamed via &amp;lt;code&amp;gt;find_each&amp;lt;/code&amp;gt;. Subclasses use &amp;lt;code&amp;gt;includes(...)&amp;lt;/code&amp;gt; to eagerly load associations and prevent N+1 queries.&lt;br /&gt;
|-&lt;br /&gt;
| '''Grouper'''&lt;br /&gt;
| A lambda &amp;lt;code&amp;gt;(row) -&amp;gt; key&amp;lt;/code&amp;gt; that determines how rows are bucketed in the accumulator state.&lt;br /&gt;
|-&lt;br /&gt;
| '''Accumulate'''&lt;br /&gt;
| Folds one row into the state in place. Contains all domain-specific math for that report.&lt;br /&gt;
|-&lt;br /&gt;
| '''Finalize'''&lt;br /&gt;
| Post-processes the finished state into the output hash. Default implementation returns state unchanged.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Memory Model ===&lt;br /&gt;
&lt;br /&gt;
State grows proportional to the number of '''groups''', such as the number of&lt;br /&gt;
distinct reviewers, not the number of raw rows.&lt;br /&gt;
&lt;br /&gt;
For example, a dataset with 10,000 responses across 20 reviewers keeps only&lt;br /&gt;
20 entries in the accumulator state at any point.&lt;br /&gt;
&lt;br /&gt;
=== Base Class Definition ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
module Reports&lt;br /&gt;
  class BaseReport&lt;br /&gt;
    def initialize(assignment)&lt;br /&gt;
      @assignment = assignment&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    def run&lt;br /&gt;
      state = initial_state&lt;br /&gt;
      source.find_each(batch_size: 500) do |row|&lt;br /&gt;
        accumulate(state, grouper.call(row), row)&lt;br /&gt;
      end&lt;br /&gt;
      finalize(state)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    private&lt;br /&gt;
&lt;br /&gt;
    def source        = raise NotImplementedError&lt;br /&gt;
    def grouper       = raise NotImplementedError&lt;br /&gt;
    def initial_state = raise NotImplementedError&lt;br /&gt;
&lt;br /&gt;
    def accumulate(_state, _key, _row)&lt;br /&gt;
      raise NotImplementedError&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    def finalize(state) = state&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Controller ==&lt;br /&gt;
&lt;br /&gt;
=== Dispatch Mechanism ===&lt;br /&gt;
&lt;br /&gt;
The controller uses a constant hash to map type strings to report classes,&lt;br /&gt;
replacing the &amp;lt;code&amp;gt;send(type)&amp;lt;/code&amp;gt; meta-programming pattern used in Repo X.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REPORT_CLASSES = {&lt;br /&gt;
  'review_response_map'          =&amp;gt; Reports::ReviewReport,&lt;br /&gt;
  'feedback_response_map'        =&amp;gt; Reports::FeedbackReport,&lt;br /&gt;
  'teammate_review_response_map' =&amp;gt; Reports::TeammateReviewReport,&lt;br /&gt;
  'bookmark_rating_response_map' =&amp;gt; Reports::BookmarkRatingReport,&lt;br /&gt;
  'basic'                        =&amp;gt; Reports::BasicReport&lt;br /&gt;
}.freeze&lt;br /&gt;
&lt;br /&gt;
def response_report&lt;br /&gt;
  type         = params.dig(:report, :type) || params[:type] || 'basic'&lt;br /&gt;
  report_class = REPORT_CLASSES[type]&lt;br /&gt;
&lt;br /&gt;
  unless report_class&lt;br /&gt;
    return render json: { error: &amp;quot;Unknown report type: #{type}&amp;quot; },&lt;br /&gt;
                  status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  assignment = Assignment.find(params[:assignment_id] || params[:id])&lt;br /&gt;
  data = report_class.new(assignment).run&lt;br /&gt;
  render json: { type: type, assignment_id: assignment.id }.merge(data)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Route ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
GET  /reports/response_report?assignment_id=&amp;lt;id&amp;gt;&amp;amp;type=&amp;lt;type&amp;gt;&lt;br /&gt;
POST /reports/response_report&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Report Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Review Report (&amp;lt;code&amp;gt;review_response_map&amp;lt;/code&amp;gt;) ===&lt;br /&gt;
&lt;br /&gt;
The review report is the most complex. It is implemented as a coordinator&lt;br /&gt;
class (&amp;lt;code&amp;gt;ReviewReport&amp;lt;/code&amp;gt;) that runs three independent inner pipelines&lt;br /&gt;
and merges their results.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pipeline !! Source !! Groups by !! Produces&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewersPipeline&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;reviewer_id&amp;lt;/code&amp;gt;&lt;br /&gt;
| Sorted reviewer list&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ScoresPipeline&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; JOIN map&lt;br /&gt;
| &amp;lt;code&amp;gt;reviewer_id&amp;lt;/code&amp;gt;&lt;br /&gt;
| Score percentage per round/reviewee&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AvgRangesPipeline&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; JOIN map&lt;br /&gt;
| &amp;lt;code&amp;gt;[reviewee_id, round]&amp;lt;/code&amp;gt;&lt;br /&gt;
| Max/min/avg per team/round&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== N+1 Fix: Precomputed Max Question Score ====&lt;br /&gt;
&lt;br /&gt;
Repo X called &amp;lt;code&amp;gt;response.maximum_score&amp;lt;/code&amp;gt; inside the accumulation loop.&lt;br /&gt;
&lt;br /&gt;
This method internally calls:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
response_assignment.assignment_questionnaires&lt;br /&gt;
  .find_by(used_in_round: round)&lt;br /&gt;
  .questionnaire&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This resulted in one query per response.&lt;br /&gt;
&lt;br /&gt;
In Repo Y, both score pipelines precompute a&lt;br /&gt;
&amp;lt;code&amp;gt;round -&amp;gt; max_question_score&amp;lt;/code&amp;gt; map with a single query before the&lt;br /&gt;
pipeline runs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def precompute_max_q_scores&lt;br /&gt;
  AssignmentQuestionnaire&lt;br /&gt;
    .joins(:questionnaire)&lt;br /&gt;
    .where(assignment_id: @assignment.id)&lt;br /&gt;
    .pluck(:used_in_round, 'questionnaires.max_question_score')&lt;br /&gt;
    .to_h&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The result, for example &amp;lt;code&amp;gt;{nil =&amp;gt; 10, 1 =&amp;gt; 10, 2 =&amp;gt; 5}&amp;lt;/code&amp;gt;, is stored in&lt;br /&gt;
&amp;lt;code&amp;gt;@max_q_score&amp;lt;/code&amp;gt; and used as a lookup inside &amp;lt;code&amp;gt;accumulate&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
max_score = total_wt * (@max_q_score[round] || @max_q_score[nil] || 1)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Sample Response ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;json&amp;quot;&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;type&amp;quot;: &amp;quot;review_response_map&amp;quot;,&lt;br /&gt;
  &amp;quot;assignment_id&amp;quot;: 1,&lt;br /&gt;
  &amp;quot;reviewers&amp;quot;: [&lt;br /&gt;
    { &amp;quot;id&amp;quot;: 5, &amp;quot;user_id&amp;quot;: 2, &amp;quot;name&amp;quot;: &amp;quot;alice&amp;quot;,&lt;br /&gt;
      &amp;quot;full_name&amp;quot;: &amp;quot;Alice Smith&amp;quot;, &amp;quot;handle&amp;quot;: &amp;quot;alice&amp;quot; }&lt;br /&gt;
  ],&lt;br /&gt;
  &amp;quot;review_scores&amp;quot;: { &amp;quot;5&amp;quot;: { &amp;quot;1&amp;quot;: { &amp;quot;3&amp;quot;: 87.5 } } },&lt;br /&gt;
  &amp;quot;avg_and_ranges&amp;quot;: { &amp;quot;3&amp;quot;: { &amp;quot;1&amp;quot;: { &amp;quot;max&amp;quot;: 92.0,&lt;br /&gt;
                                    &amp;quot;min&amp;quot;: 75.0,&lt;br /&gt;
                                    &amp;quot;avg&amp;quot;: 83.5 } } }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Feedback Report (&amp;lt;code&amp;gt;feedback_response_map&amp;lt;/code&amp;gt;) ===&lt;br /&gt;
&lt;br /&gt;
Produces the list of authors and the IDs of review responses that received&lt;br /&gt;
author feedback, bucketed by round for varying-rubric assignments.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def source&lt;br /&gt;
  Response&lt;br /&gt;
    .joins(:response_map)&lt;br /&gt;
    .where(&lt;br /&gt;
      response_maps: { type: 'ReviewResponseMap',&lt;br /&gt;
                       reviewed_object_id: @assignment.id }&lt;br /&gt;
    )&lt;br /&gt;
    .order(created_at: :desc)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
def grouper       = -&amp;gt;(r) { [r.map_id, r.round] }&lt;br /&gt;
def initial_state = { seen: Set.new, round_1: [],&lt;br /&gt;
                      round_2: [], round_3: [], all: [] }&lt;br /&gt;
&lt;br /&gt;
def accumulate(state, key, response)&lt;br /&gt;
  return if state[:seen].include?(key)&lt;br /&gt;
  state[:seen].add(key)&lt;br /&gt;
  if @assignment.varying_rubrics_by_round?&lt;br /&gt;
    case response.round&lt;br /&gt;
    when 1 then state[:round_1] &amp;lt;&amp;lt; response.id&lt;br /&gt;
    when 2 then state[:round_2] &amp;lt;&amp;lt; response.id&lt;br /&gt;
    when 3 then state[:round_3] &amp;lt;&amp;lt; response.id&lt;br /&gt;
    end&lt;br /&gt;
  else&lt;br /&gt;
    state[:all] &amp;lt;&amp;lt; response.id&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deduplication uses a &amp;lt;code&amp;gt;Set&amp;lt;/code&amp;gt;, which gives O(1) lookup, rather than the&lt;br /&gt;
array-based &amp;lt;code&amp;gt;seen_map_round_keys.include?&amp;lt;/code&amp;gt; from Repo X, which gives&lt;br /&gt;
O(n) lookup.&lt;br /&gt;
&lt;br /&gt;
Authors are fetched once in &amp;lt;code&amp;gt;finalize&amp;lt;/code&amp;gt;, not inside the stream.&lt;br /&gt;
&lt;br /&gt;
==== End-to-End Execution Flow ====&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;run&amp;lt;/code&amp;gt; method is '''inherited from &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&amp;lt;code&amp;gt;FeedbackReport&amp;lt;/code&amp;gt; never defines it.&lt;br /&gt;
&lt;br /&gt;
Calling &amp;lt;code&amp;gt;FeedbackReport.new(assignment).run&amp;lt;/code&amp;gt; triggers the following&lt;br /&gt;
sequence:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
ReportsController&lt;br /&gt;
  REPORT_CLASSES['feedback_response_map'].new(assignment).run&lt;br /&gt;
    |&lt;br /&gt;
    |  (inherited from BaseReport)&lt;br /&gt;
    v&lt;br /&gt;
  BaseReport#run&lt;br /&gt;
    |&lt;br /&gt;
    |-- Step 1: state = initial_state&lt;br /&gt;
    |       =&amp;gt; { seen: Set.new,&lt;br /&gt;
    |            round_1: [], round_2: [], round_3: [], all: [] }&lt;br /&gt;
    |&lt;br /&gt;
    |-- Step 2: source.find_each(batch_size: 500)&lt;br /&gt;
    |       =&amp;gt; Response.joins(:response_map)&lt;br /&gt;
    |                  .where(type: 'ReviewResponseMap',&lt;br /&gt;
    |                         reviewed_object_id: assignment.id)&lt;br /&gt;
    |                  .order(created_at: :desc)&lt;br /&gt;
    |          streams Response records newest-first, in batches&lt;br /&gt;
    |&lt;br /&gt;
    |-- Step 3: for each Response row:&lt;br /&gt;
    |       key = grouper.call(row)&lt;br /&gt;
    |           =&amp;gt; [row.map_id, row.round]   e.g. [42, 1]&lt;br /&gt;
    |&lt;br /&gt;
    |       accumulate(state, key, row)&lt;br /&gt;
    |           =&amp;gt; skip if state[:seen] already has [map_id, round]&lt;br /&gt;
    |              (keeps only the latest response per map per round&lt;br /&gt;
    |               because source is ordered newest-first)&lt;br /&gt;
    |           =&amp;gt; otherwise: add key to :seen, then bucket row.id:&lt;br /&gt;
    |                round == 1  =&amp;gt;  state[:round_1] &amp;lt;&amp;lt; row.id&lt;br /&gt;
    |                round == 2  =&amp;gt;  state[:round_2] &amp;lt;&amp;lt; row.id&lt;br /&gt;
    |                round == 3  =&amp;gt;  state[:round_3] &amp;lt;&amp;lt; row.id&lt;br /&gt;
    |                (or state[:all] if single-rubric assignment)&lt;br /&gt;
    |&lt;br /&gt;
    |-- Step 4: finalize(state)&lt;br /&gt;
            =&amp;gt; fetch_authors  (one query: teams -&amp;gt; users -&amp;gt; participants)&lt;br /&gt;
            =&amp;gt; if varying_rubrics_by_round?&lt;br /&gt;
                 return { authors: [...],&lt;br /&gt;
                          review_response_ids: {&lt;br /&gt;
                            round_1: [...], round_2: [...], round_3: [...] } }&lt;br /&gt;
               else&lt;br /&gt;
                 return { authors: [...],&lt;br /&gt;
                          review_response_ids: [...] }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The key point is that &amp;lt;code&amp;gt;FeedbackReport&amp;lt;/code&amp;gt; only defines the four pieces&lt;br /&gt;
the pipeline needs:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;source&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;grouper&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;initial_state&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;accumulate&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;finalize&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ruby's inheritance mechanism means calling &amp;lt;code&amp;gt;.run&amp;lt;/code&amp;gt; on a&lt;br /&gt;
&amp;lt;code&amp;gt;FeedbackReport&amp;lt;/code&amp;gt; instance automatically executes&lt;br /&gt;
&amp;lt;code&amp;gt;BaseReport#run&amp;lt;/code&amp;gt;, which calls back into &amp;lt;code&amp;gt;FeedbackReport&amp;lt;/code&amp;gt;'s&lt;br /&gt;
implementations of those methods.&lt;br /&gt;
&lt;br /&gt;
==== Sample Response (varying rubrics) ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;json&amp;quot;&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;type&amp;quot;: &amp;quot;feedback_response_map&amp;quot;,&lt;br /&gt;
  &amp;quot;authors&amp;quot;: [{ &amp;quot;id&amp;quot;: 7, &amp;quot;name&amp;quot;: &amp;quot;bob&amp;quot;, &amp;quot;full_name&amp;quot;: &amp;quot;Bob Jones&amp;quot; }],&lt;br /&gt;
  &amp;quot;review_response_ids&amp;quot;: {&lt;br /&gt;
    &amp;quot;round_1&amp;quot;: [12, 15], &amp;quot;round_2&amp;quot;: [18], &amp;quot;round_3&amp;quot;: []&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Teammate Review Report (&amp;lt;code&amp;gt;teammate_review_response_map&amp;lt;/code&amp;gt;) ===&lt;br /&gt;
&lt;br /&gt;
Streams &amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; records grouped by&lt;br /&gt;
&amp;lt;code&amp;gt;reviewer_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The first occurrence per reviewer is kept using deduplication via early return&lt;br /&gt;
if the key already exists in state. Reviewer associations are eagerly loaded.&lt;br /&gt;
&lt;br /&gt;
==== Sample Response ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;json&amp;quot;&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;type&amp;quot;: &amp;quot;teammate_review_response_map&amp;quot;,&lt;br /&gt;
  &amp;quot;reviewers&amp;quot;: [&lt;br /&gt;
    { &amp;quot;reviewer_id&amp;quot;: 5, &amp;quot;user_id&amp;quot;: 2,&lt;br /&gt;
      &amp;quot;name&amp;quot;: &amp;quot;alice&amp;quot;, &amp;quot;full_name&amp;quot;: &amp;quot;Alice Smith&amp;quot; }&lt;br /&gt;
  ]&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Bookmark Rating Report (&amp;lt;code&amp;gt;bookmark_rating_response_map&amp;lt;/code&amp;gt;) ===&lt;br /&gt;
&lt;br /&gt;
Streams &amp;lt;code&amp;gt;BookmarkRatingResponseMap&amp;lt;/code&amp;gt; records, accumulating distinct&lt;br /&gt;
bookmark IDs into a &amp;lt;code&amp;gt;Set&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Project topics are fetched once in &amp;lt;code&amp;gt;finalize&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Bug Fixed During Port ====&lt;br /&gt;
&lt;br /&gt;
The model's &amp;lt;code&amp;gt;bookmark_response_report&amp;lt;/code&amp;gt; in Repo Y was incorrectly&lt;br /&gt;
calling:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
.pluck(:reviewed_object_id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns assignment IDs, since &amp;lt;code&amp;gt;reviewed_object_id&amp;lt;/code&amp;gt; is the&lt;br /&gt;
foreign key to &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Bookmark IDs are stored in &amp;lt;code&amp;gt;reviewee_id&amp;lt;/code&amp;gt;. Therefore, this was fixed&lt;br /&gt;
to:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
.pluck(:reviewee_id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Sample Response ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;json&amp;quot;&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;type&amp;quot;: &amp;quot;bookmark_rating_response_map&amp;quot;,&lt;br /&gt;
  &amp;quot;bookmark_ids&amp;quot;: [10, 14, 22],&lt;br /&gt;
  &amp;quot;topics&amp;quot;: [{ &amp;quot;id&amp;quot;: 3, &amp;quot;topic_name&amp;quot;: &amp;quot;Machine Learning&amp;quot; }]&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Basic Report (&amp;lt;code&amp;gt;basic&amp;lt;/code&amp;gt;) ===&lt;br /&gt;
&lt;br /&gt;
Returns minimal assignment metadata.&lt;br /&gt;
&lt;br /&gt;
No streaming is required since all data comes from the already-loaded&lt;br /&gt;
&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; object.&lt;br /&gt;
&lt;br /&gt;
This report is used as the default when no type parameter is provided.&lt;br /&gt;
&lt;br /&gt;
==== Sample Response ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;json&amp;quot;&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,&lt;br /&gt;
  &amp;quot;assignment_id&amp;quot;: 1,&lt;br /&gt;
  &amp;quot;assignment&amp;quot;: {&lt;br /&gt;
    &amp;quot;id&amp;quot;: 1, &amp;quot;name&amp;quot;: &amp;quot;Project 1&amp;quot;,&lt;br /&gt;
    &amp;quot;num_review_rounds&amp;quot;: 2,&lt;br /&gt;
    &amp;quot;varying_rubrics_by_round&amp;quot;: true&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== File Structure ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
app/&lt;br /&gt;
  controllers/&lt;br /&gt;
    reports_controller.rb        Entry point, REPORT_CLASSES dispatch&lt;br /&gt;
  helpers/&lt;br /&gt;
    report_formatter_helper.rb   Empty namespace (logic moved to services)&lt;br /&gt;
  services/&lt;br /&gt;
    reports/&lt;br /&gt;
      base_report.rb             Abstract pipeline template&lt;br /&gt;
      review_report.rb           3-pipeline coordinator&lt;br /&gt;
      feedback_report.rb         Single pipeline, round bucketing&lt;br /&gt;
      teammate_review_report.rb  Single pipeline&lt;br /&gt;
      bookmark_rating_report.rb  Single pipeline&lt;br /&gt;
      basic_report.rb            Simple struct&lt;br /&gt;
  models/&lt;br /&gt;
    review_response_map.rb&lt;br /&gt;
      .review_response_report class method&lt;br /&gt;
    feedback_response_map.rb&lt;br /&gt;
      .feedback_response_report class method&lt;br /&gt;
    teammate_review_response_map.rb&lt;br /&gt;
      .teammate_response_report class method&lt;br /&gt;
    bookmark_rating_response_map.rb&lt;br /&gt;
      .bookmark_response_report (bug fixed)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Blocked Report Types ==&lt;br /&gt;
&lt;br /&gt;
The following report types exist in Repo X but cannot yet be implemented in&lt;br /&gt;
Repo Y due to missing database tables or models.&lt;br /&gt;
&lt;br /&gt;
Each is ready to be added once its dependency is ported.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Report Type !! Missing Dependency !! Repo X Location&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;calibration&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;calibrate_to&amp;lt;/code&amp;gt; column on &amp;lt;code&amp;gt;response_maps&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;report_formatter_helper.rb&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;self_review&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;SelfReviewResponseMap&amp;lt;/code&amp;gt; model&lt;br /&gt;
| &amp;lt;code&amp;gt;self_review_response_map.rb&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;survey&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;survey_deployments&amp;lt;/code&amp;gt; table&lt;br /&gt;
| &amp;lt;code&amp;gt;survey_response_map.rb&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;quiz&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;quiz_responses&amp;lt;/code&amp;gt; table&lt;br /&gt;
| &amp;lt;code&amp;gt;quiz_response_map.rb&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;answer_tagging&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;tag_prompt_deployments&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;answer_tags&amp;lt;/code&amp;gt; tables&lt;br /&gt;
| &amp;lt;code&amp;gt;tag_prompt_deployment.rb&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
To add a blocked report once its dependencies are available:&lt;br /&gt;
&lt;br /&gt;
# Create &amp;lt;code&amp;gt;app/services/reports/&amp;amp;lt;name&amp;amp;gt;_report.rb&amp;lt;/code&amp;gt; inheriting &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Define &amp;lt;code&amp;gt;source&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;grouper&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;initial_state&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;finalize&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Add an entry to &amp;lt;code&amp;gt;ReportsController::REPORT_CLASSES&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Comparison with Repo X ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Concern !! Repo X !! Repo Y&lt;br /&gt;
|-&lt;br /&gt;
| Output format&lt;br /&gt;
| ERB instance variables, such as &amp;lt;code&amp;gt;@reviewers&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;@review_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
| JSON hash from &amp;lt;code&amp;gt;report.run&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Loading strategy&lt;br /&gt;
| All records loaded into arrays at once&lt;br /&gt;
| &amp;lt;code&amp;gt;find_each&amp;lt;/code&amp;gt; batched streaming&lt;br /&gt;
|-&lt;br /&gt;
| Metrics location&lt;br /&gt;
| &amp;lt;code&amp;gt;compute_metrics&amp;lt;/code&amp;gt; in helper base&lt;br /&gt;
| Each report owns &amp;lt;code&amp;gt;accumulate&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;finalize&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Dispatch&lt;br /&gt;
| &amp;lt;code&amp;gt;send(@type.underscore, params, session)&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;REPORT_CLASSES[type].new(assignment).run&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| N+1 on scores&lt;br /&gt;
| &amp;lt;code&amp;gt;response.maximum_score&amp;lt;/code&amp;gt; per row — questionnaire lookup each time&lt;br /&gt;
| Precomputed &amp;lt;code&amp;gt;round-&amp;gt;max_score&amp;lt;/code&amp;gt; map, one query before pipeline&lt;br /&gt;
|-&lt;br /&gt;
| Deduplication&lt;br /&gt;
| &amp;lt;code&amp;gt;Array#include?&amp;lt;/code&amp;gt; — O(n) per check&lt;br /&gt;
| &amp;lt;code&amp;gt;Set#include?&amp;lt;/code&amp;gt; — O(1) per check&lt;br /&gt;
|-&lt;br /&gt;
| Additional features&lt;br /&gt;
| LLM evaluation, CSV export, calibration, self-review, survey, quiz, answer tagging&lt;br /&gt;
| Not yet ported; blocked on schema&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
== Author ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Name !! Role&lt;br /&gt;
|-&lt;br /&gt;
| '''Aanand Sreekumaran Nair Jayakumari'''&lt;br /&gt;
| Project contributor / developer&lt;br /&gt;
|}&lt;/div&gt;</description>
			<pubDate>Sat, 02 May 2026 21:51:39 GMT</pubDate>
			<dc:creator>Asreeku</dc:creator>
			<comments>https://wiki.expertiza.ncsu.edu/index.php?title=Talk:Report_Generation_Framework_(WIP)</comments>
		</item>
		<item>
			<title>Main Page</title>
			<link>https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page&amp;diff=168198&amp;oldid=168196</link>
			<guid isPermaLink="false">https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page&amp;diff=168198&amp;oldid=168196</guid>
			<description>&lt;p&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #fff; color: #202122;&quot; data-mw=&quot;interface&quot;&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;tr class=&quot;diff-title&quot; lang=&quot;en&quot;&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;← Older revision&lt;/td&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;Revision as of 21:49, 2 May 2026&lt;/td&gt;
				&lt;/tr&gt;&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l41&quot;&gt;Line 41:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 41:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[ReactJs Frontend]]&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[ReactJs Frontend]]&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[Front-End/Back-End]]&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[Front-End/Back-End]]&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[Report &lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;generation framework&lt;/del&gt;(WIP)]]&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[Report &lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Generation Framework &lt;/ins&gt;(WIP)]]&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;==Application Behavior==&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;==Application Behavior==&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;</description>
			<pubDate>Sat, 02 May 2026 21:49:59 GMT</pubDate>
			<dc:creator>Asreeku</dc:creator>
			<comments>https://wiki.expertiza.ncsu.edu/index.php?title=Talk:Main_Page</comments>
		</item>
		<item>
			<title>Report generation framework(WIP)</title>
			<link>https://wiki.expertiza.ncsu.edu/index.php?title=Report_generation_framework(WIP)&amp;diff=168197&amp;oldid=0</link>
			<guid isPermaLink="false">https://wiki.expertiza.ncsu.edu/index.php?title=Report_generation_framework(WIP)&amp;diff=168197&amp;oldid=0</guid>
			<description>&lt;p&gt;Created page with &amp;quot;= Report Generation = == Design and Implementation Documentation == === Expertiza Reimplementation Back-End ===  ----  == Overview ==  The reporting subsystem was ported from the original Expertiza codebase (referred to as &amp;#039;&amp;#039;&amp;#039;Repo X&amp;#039;&amp;#039;&amp;#039;) into the reimplemented Rails API back-end (referred to as &amp;#039;&amp;#039;&amp;#039;Repo Y&amp;#039;&amp;#039;&amp;#039;) and redesigned in the process.  Repo X used a Rails helper module (&amp;lt;code&amp;gt;ReportFormatterHelper&amp;lt;/code&amp;gt;) that assigned instance variables, such as &amp;lt;code&amp;gt;@reviewers&amp;lt;/cod...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;= Report Generation =&lt;br /&gt;
== Design and Implementation Documentation ==&lt;br /&gt;
=== Expertiza Reimplementation Back-End ===&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Overview ==&lt;br /&gt;
&lt;br /&gt;
The reporting subsystem was ported from the original Expertiza codebase&lt;br /&gt;
(referred to as '''Repo X''') into the reimplemented Rails API back-end&lt;br /&gt;
(referred to as '''Repo Y''') and redesigned in the process.&lt;br /&gt;
&lt;br /&gt;
Repo X used a Rails helper module (&amp;lt;code&amp;gt;ReportFormatterHelper&amp;lt;/code&amp;gt;) that&lt;br /&gt;
assigned instance variables, such as &amp;lt;code&amp;gt;@reviewers&amp;lt;/code&amp;gt; and&lt;br /&gt;
&amp;lt;code&amp;gt;@review_scores&amp;lt;/code&amp;gt;, for ERB views.&lt;br /&gt;
&lt;br /&gt;
Since Repo Y is a JSON API with no views, instance variables are not applicable.&lt;br /&gt;
The architecture was therefore redesigned around a '''streaming reduce pipeline'''&lt;br /&gt;
that returns plain Ruby hashes rendered as JSON.&lt;br /&gt;
&lt;br /&gt;
=== Design Goals ===&lt;br /&gt;
&lt;br /&gt;
* Avoid loading entire result sets into memory at once.&lt;br /&gt;
* Keep domain-specific computation out of the base class.&lt;br /&gt;
* Make each report type independently composable and testable.&lt;br /&gt;
* Fix N+1 query patterns present in Repo X.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Anti-Patterns Addressed ==&lt;br /&gt;
&lt;br /&gt;
The design directly addresses two anti-patterns identified during architectural&lt;br /&gt;
review of Repo X.&lt;br /&gt;
&lt;br /&gt;
=== The &amp;lt;code&amp;gt;fetch_responses&amp;lt;/code&amp;gt; Anti-Pattern ===&lt;br /&gt;
&lt;br /&gt;
Repo X loaded all response records into an unnamed, ad-hoc array before&lt;br /&gt;
processing:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# WRONG&lt;br /&gt;
responses = fetch_responses   # full memory load&lt;br /&gt;
grouped   = group(responses)&lt;br /&gt;
metrics   = compute_metrics(grouped)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This forces the entire result set into Ruby memory, prevents streaming, and&lt;br /&gt;
makes the intermediate structure implicit.&lt;br /&gt;
&lt;br /&gt;
The fix is to '''never materialise all rows at once'''. Instead, use&lt;br /&gt;
&amp;lt;code&amp;gt;find_each&amp;lt;/code&amp;gt; to stream records in batches, so memory usage scales&lt;br /&gt;
with the number of '''groups''', not the number of raw rows.&lt;br /&gt;
&lt;br /&gt;
=== Default Metrics in the Base Class ===&lt;br /&gt;
&lt;br /&gt;
Repo X placed domain-specific math directly in the base class:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
# WRONG — in BaseReport&lt;br /&gt;
def compute_metrics(grouped)&lt;br /&gt;
  grouped.transform_values do |responses|&lt;br /&gt;
    {&lt;br /&gt;
      count:     responses.size,&lt;br /&gt;
      avg_score: responses.map(&amp;amp;:score).sum / responses.size.to_f&lt;br /&gt;
    }&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This ties every subclass to one particular shape of computation. The fix is&lt;br /&gt;
to move all domain math into per-report accumulator logic. The base class&lt;br /&gt;
contains '''zero''' domain math.&lt;br /&gt;
&lt;br /&gt;
==== What &amp;quot;Domain Math&amp;quot; Means ====&lt;br /&gt;
&lt;br /&gt;
'''Domain math''' refers to the business-logic calculations specific to a&lt;br /&gt;
particular report type — the actual formulas and aggregations that answer what&lt;br /&gt;
the report is trying to show.&lt;br /&gt;
&lt;br /&gt;
It is called &amp;quot;domain&amp;quot; math because it belongs to the problem domain&lt;br /&gt;
(peer assessment), not to the generic pipeline machinery.&lt;br /&gt;
&lt;br /&gt;
Each report type has its own domain math:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Report !! Its domain math&lt;br /&gt;
|-&lt;br /&gt;
| Review scores&lt;br /&gt;
| &amp;lt;code&amp;gt;(raw_score / max_score) * 100&amp;lt;/code&amp;gt; — percentage score per reviewer per round&lt;br /&gt;
|-&lt;br /&gt;
| Avg/ranges&lt;br /&gt;
| &amp;lt;code&amp;gt;max&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;min&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;sum / size&amp;lt;/code&amp;gt; — score aggregates across a team's reviewers&lt;br /&gt;
|-&lt;br /&gt;
| Feedback&lt;br /&gt;
| Bucket response IDs into &amp;lt;code&amp;gt;round_1&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;round_2&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;round_3&amp;lt;/code&amp;gt; arrays&lt;br /&gt;
|-&lt;br /&gt;
| Bookmark rating&lt;br /&gt;
| Collect distinct bookmark IDs into a &amp;lt;code&amp;gt;Set&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
Notice that these are completely different in shape: one computes a percentage,&lt;br /&gt;
another computes min/max/avg, and another just collects IDs.&lt;br /&gt;
&lt;br /&gt;
If &amp;lt;code&amp;gt;avg_score&amp;lt;/code&amp;gt; lived in &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt;, every subclass&lt;br /&gt;
would either inherit math it does not need — for example, a bookmark report has&lt;br /&gt;
no scores — or be forced to override the method just to suppress it.&lt;br /&gt;
&lt;br /&gt;
The rule is therefore:&lt;br /&gt;
&lt;br /&gt;
&amp;gt; &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt; only defines '''how to run the pipeline''' — stream, group, fold, finalize.  &lt;br /&gt;
&amp;gt; The '''what to compute''' belongs entirely inside each report's own &amp;lt;code&amp;gt;accumulate&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;finalize&amp;lt;/code&amp;gt; methods.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== System Diagrams [To be updated] ==&lt;br /&gt;
&lt;br /&gt;
=== Overall Request Flow ===&lt;br /&gt;
&lt;br /&gt;
The diagram below shows how an incoming HTTP request travels through the&lt;br /&gt;
system from the controller down to the pipeline and back out as JSON.&lt;br /&gt;
&lt;br /&gt;
Since MediaWiki does not render TikZ directly, this diagram is represented&lt;br /&gt;
as a text-based flow.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
HTTP Client (Front-end)&lt;br /&gt;
        |&lt;br /&gt;
        | GET /reports/response_report?assignment_id=&amp;amp;type=&lt;br /&gt;
        v&lt;br /&gt;
ReportsController#response_report&lt;br /&gt;
        |&lt;br /&gt;
        | params[:type]&lt;br /&gt;
        v&lt;br /&gt;
REPORT_CLASSES[type]&lt;br /&gt;
look up concrete class&lt;br /&gt;
        |&lt;br /&gt;
        | .new(assignment).run&lt;br /&gt;
        v&lt;br /&gt;
Concrete Report&lt;br /&gt;
for example, FeedbackReport&lt;br /&gt;
        |&lt;br /&gt;
        | inherits run&lt;br /&gt;
        v&lt;br /&gt;
BaseReport#run&lt;br /&gt;
inherited find_each streaming loop&lt;br /&gt;
        |&lt;br /&gt;
        | calls subclass methods&lt;br /&gt;
        v&lt;br /&gt;
source -&amp;gt; grouper -&amp;gt; accumulate -&amp;gt; finalize&lt;br /&gt;
        |&lt;br /&gt;
        | output hash&lt;br /&gt;
        v&lt;br /&gt;
render json: { ... }&lt;br /&gt;
        |&lt;br /&gt;
        | JSON response&lt;br /&gt;
        v&lt;br /&gt;
HTTP Client (Front-end)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
''Figure: End-to-end request flow for report generation''&lt;br /&gt;
&lt;br /&gt;
=== Pipeline Internals ===&lt;br /&gt;
&lt;br /&gt;
This diagram shows the four stages inside &amp;lt;code&amp;gt;BaseReport#run&amp;lt;/code&amp;gt;.&lt;br /&gt;
The stages are defined by each concrete subclass; the pipeline loop itself&lt;br /&gt;
never changes.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
+------------------+        +------------------+&lt;br /&gt;
| 1. Source        | rows   | 2. Grouper       |&lt;br /&gt;
| AR relation      | -----&amp;gt; | lambda: row-&amp;gt;key |&lt;br /&gt;
| streamed via     |        | e.g. reviewer_id |&lt;br /&gt;
| find_each        |        +------------------+&lt;br /&gt;
+------------------+                 |&lt;br /&gt;
                                     | key, row&lt;br /&gt;
                                     v&lt;br /&gt;
+------------------+        +------------------+&lt;br /&gt;
| 4. Finalize      | state  | 3. Accumulate    |&lt;br /&gt;
| shape state into | &amp;lt;----- | fold row into    |&lt;br /&gt;
| output hash      |        | state            |&lt;br /&gt;
+------------------+        | domain math here |&lt;br /&gt;
        |                   +------------------+&lt;br /&gt;
        |&lt;br /&gt;
        v&lt;br /&gt;
Hash -&amp;gt; JSON&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Additional notes:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;source&amp;lt;/code&amp;gt; uses &amp;lt;code&amp;gt;includes(...)&amp;lt;/code&amp;gt; where needed to avoid N+1 queries.&lt;br /&gt;
* &amp;lt;code&amp;gt;accumulate&amp;lt;/code&amp;gt; handles scores, deduplication, bucketing, and counting depending on the report type.&lt;br /&gt;
&lt;br /&gt;
''Figure: The four stages every report passes through inside BaseReport#run''&lt;br /&gt;
&lt;br /&gt;
=== Class Hierarchy ===&lt;br /&gt;
&lt;br /&gt;
This diagram shows how concrete report classes relate to &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Solid inheritance from &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt; is represented by indentation.&lt;br /&gt;
Composition from &amp;lt;code&amp;gt;ReviewReport&amp;lt;/code&amp;gt; to its inner pipelines is represented&lt;br /&gt;
under the coordinator.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
BaseReport&lt;br /&gt;
|&lt;br /&gt;
|-- ReviewReport (coordinator)&lt;br /&gt;
|     |&lt;br /&gt;
|     |-- ReviewersPipeline&lt;br /&gt;
|     |-- ScoresPipeline&lt;br /&gt;
|     |-- AvgRangesPipeline&lt;br /&gt;
|&lt;br /&gt;
|-- FeedbackReport&lt;br /&gt;
|-- TeammateReviewReport&lt;br /&gt;
|-- BookmarkRatingReport&lt;br /&gt;
|-- BasicReport&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Legend:&lt;br /&gt;
&lt;br /&gt;
* Direct child under &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt; = inherits &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt;&lt;br /&gt;
* Pipelines under &amp;lt;code&amp;gt;ReviewReport&amp;lt;/code&amp;gt; = coordinator runs inner pipeline&lt;br /&gt;
* The inner pipelines also inherit &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
''Figure: Class hierarchy for the report generation subsystem''&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Architecture: The Streaming Pipeline ==&lt;br /&gt;
&lt;br /&gt;
=== Pipeline Shape ===&lt;br /&gt;
&lt;br /&gt;
All reports are built on a single pipeline template defined in&lt;br /&gt;
&amp;lt;code&amp;gt;Reports::BaseReport&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def run&lt;br /&gt;
  state = initial_state&lt;br /&gt;
  source.find_each(batch_size: 500) do |row|&lt;br /&gt;
    accumulate(state, grouper.call(row), row)&lt;br /&gt;
  end&lt;br /&gt;
  finalize(state)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The pipeline consists of four concerns:&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Concern !! Responsibility&lt;br /&gt;
|-&lt;br /&gt;
| '''Source'''&lt;br /&gt;
| ActiveRecord relation streamed via &amp;lt;code&amp;gt;find_each&amp;lt;/code&amp;gt;. Subclasses use &amp;lt;code&amp;gt;includes(...)&amp;lt;/code&amp;gt; to eagerly load associations and prevent N+1 queries.&lt;br /&gt;
|-&lt;br /&gt;
| '''Grouper'''&lt;br /&gt;
| A lambda &amp;lt;code&amp;gt;(row) -&amp;gt; key&amp;lt;/code&amp;gt; that determines how rows are bucketed in the accumulator state.&lt;br /&gt;
|-&lt;br /&gt;
| '''Accumulate'''&lt;br /&gt;
| Folds one row into the state in place. Contains all domain-specific math for that report.&lt;br /&gt;
|-&lt;br /&gt;
| '''Finalize'''&lt;br /&gt;
| Post-processes the finished state into the output hash. Default implementation returns state unchanged.&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
=== Memory Model ===&lt;br /&gt;
&lt;br /&gt;
State grows proportional to the number of '''groups''', such as the number of&lt;br /&gt;
distinct reviewers, not the number of raw rows.&lt;br /&gt;
&lt;br /&gt;
For example, a dataset with 10,000 responses across 20 reviewers keeps only&lt;br /&gt;
20 entries in the accumulator state at any point.&lt;br /&gt;
&lt;br /&gt;
=== Base Class Definition ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
module Reports&lt;br /&gt;
  class BaseReport&lt;br /&gt;
    def initialize(assignment)&lt;br /&gt;
      @assignment = assignment&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    def run&lt;br /&gt;
      state = initial_state&lt;br /&gt;
      source.find_each(batch_size: 500) do |row|&lt;br /&gt;
        accumulate(state, grouper.call(row), row)&lt;br /&gt;
      end&lt;br /&gt;
      finalize(state)&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    private&lt;br /&gt;
&lt;br /&gt;
    def source        = raise NotImplementedError&lt;br /&gt;
    def grouper       = raise NotImplementedError&lt;br /&gt;
    def initial_state = raise NotImplementedError&lt;br /&gt;
&lt;br /&gt;
    def accumulate(_state, _key, _row)&lt;br /&gt;
      raise NotImplementedError&lt;br /&gt;
    end&lt;br /&gt;
&lt;br /&gt;
    def finalize(state) = state&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Controller ==&lt;br /&gt;
&lt;br /&gt;
=== Dispatch Mechanism ===&lt;br /&gt;
&lt;br /&gt;
The controller uses a constant hash to map type strings to report classes,&lt;br /&gt;
replacing the &amp;lt;code&amp;gt;send(type)&amp;lt;/code&amp;gt; meta-programming pattern used in Repo X.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
REPORT_CLASSES = {&lt;br /&gt;
  'review_response_map'          =&amp;gt; Reports::ReviewReport,&lt;br /&gt;
  'feedback_response_map'        =&amp;gt; Reports::FeedbackReport,&lt;br /&gt;
  'teammate_review_response_map' =&amp;gt; Reports::TeammateReviewReport,&lt;br /&gt;
  'bookmark_rating_response_map' =&amp;gt; Reports::BookmarkRatingReport,&lt;br /&gt;
  'basic'                        =&amp;gt; Reports::BasicReport&lt;br /&gt;
}.freeze&lt;br /&gt;
&lt;br /&gt;
def response_report&lt;br /&gt;
  type         = params.dig(:report, :type) || params[:type] || 'basic'&lt;br /&gt;
  report_class = REPORT_CLASSES[type]&lt;br /&gt;
&lt;br /&gt;
  unless report_class&lt;br /&gt;
    return render json: { error: &amp;quot;Unknown report type: #{type}&amp;quot; },&lt;br /&gt;
                  status: :unprocessable_entity&lt;br /&gt;
  end&lt;br /&gt;
&lt;br /&gt;
  assignment = Assignment.find(params[:assignment_id] || params[:id])&lt;br /&gt;
  data = report_class.new(assignment).run&lt;br /&gt;
  render json: { type: type, assignment_id: assignment.id }.merge(data)&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
=== Route ===&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
GET  /reports/response_report?assignment_id=&amp;lt;id&amp;gt;&amp;amp;type=&amp;lt;type&amp;gt;&lt;br /&gt;
POST /reports/response_report&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Report Implementations ==&lt;br /&gt;
&lt;br /&gt;
=== Review Report (&amp;lt;code&amp;gt;review_response_map&amp;lt;/code&amp;gt;) ===&lt;br /&gt;
&lt;br /&gt;
The review report is the most complex. It is implemented as a coordinator&lt;br /&gt;
class (&amp;lt;code&amp;gt;ReviewReport&amp;lt;/code&amp;gt;) that runs three independent inner pipelines&lt;br /&gt;
and merges their results.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Pipeline !! Source !! Groups by !! Produces&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewersPipeline&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;ReviewResponseMap&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;reviewer_id&amp;lt;/code&amp;gt;&lt;br /&gt;
| Sorted reviewer list&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;ScoresPipeline&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; JOIN map&lt;br /&gt;
| &amp;lt;code&amp;gt;reviewer_id&amp;lt;/code&amp;gt;&lt;br /&gt;
| Score percentage per round/reviewee&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;AvgRangesPipeline&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;Response&amp;lt;/code&amp;gt; JOIN map&lt;br /&gt;
| &amp;lt;code&amp;gt;[reviewee_id, round]&amp;lt;/code&amp;gt;&lt;br /&gt;
| Max/min/avg per team/round&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
==== N+1 Fix: Precomputed Max Question Score ====&lt;br /&gt;
&lt;br /&gt;
Repo X called &amp;lt;code&amp;gt;response.maximum_score&amp;lt;/code&amp;gt; inside the accumulation loop.&lt;br /&gt;
&lt;br /&gt;
This method internally calls:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
response_assignment.assignment_questionnaires&lt;br /&gt;
  .find_by(used_in_round: round)&lt;br /&gt;
  .questionnaire&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This resulted in one query per response.&lt;br /&gt;
&lt;br /&gt;
In Repo Y, both score pipelines precompute a&lt;br /&gt;
&amp;lt;code&amp;gt;round -&amp;gt; max_question_score&amp;lt;/code&amp;gt; map with a single query before the&lt;br /&gt;
pipeline runs:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def precompute_max_q_scores&lt;br /&gt;
  AssignmentQuestionnaire&lt;br /&gt;
    .joins(:questionnaire)&lt;br /&gt;
    .where(assignment_id: @assignment.id)&lt;br /&gt;
    .pluck(:used_in_round, 'questionnaires.max_question_score')&lt;br /&gt;
    .to_h&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The result, for example &amp;lt;code&amp;gt;{nil =&amp;gt; 10, 1 =&amp;gt; 10, 2 =&amp;gt; 5}&amp;lt;/code&amp;gt;, is stored in&lt;br /&gt;
&amp;lt;code&amp;gt;@max_q_score&amp;lt;/code&amp;gt; and used as a lookup inside &amp;lt;code&amp;gt;accumulate&amp;lt;/code&amp;gt;:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
max_score = total_wt * (@max_q_score[round] || @max_q_score[nil] || 1)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Sample Response ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;json&amp;quot;&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;type&amp;quot;: &amp;quot;review_response_map&amp;quot;,&lt;br /&gt;
  &amp;quot;assignment_id&amp;quot;: 1,&lt;br /&gt;
  &amp;quot;reviewers&amp;quot;: [&lt;br /&gt;
    { &amp;quot;id&amp;quot;: 5, &amp;quot;user_id&amp;quot;: 2, &amp;quot;name&amp;quot;: &amp;quot;alice&amp;quot;,&lt;br /&gt;
      &amp;quot;full_name&amp;quot;: &amp;quot;Alice Smith&amp;quot;, &amp;quot;handle&amp;quot;: &amp;quot;alice&amp;quot; }&lt;br /&gt;
  ],&lt;br /&gt;
  &amp;quot;review_scores&amp;quot;: { &amp;quot;5&amp;quot;: { &amp;quot;1&amp;quot;: { &amp;quot;3&amp;quot;: 87.5 } } },&lt;br /&gt;
  &amp;quot;avg_and_ranges&amp;quot;: { &amp;quot;3&amp;quot;: { &amp;quot;1&amp;quot;: { &amp;quot;max&amp;quot;: 92.0,&lt;br /&gt;
                                    &amp;quot;min&amp;quot;: 75.0,&lt;br /&gt;
                                    &amp;quot;avg&amp;quot;: 83.5 } } }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Feedback Report (&amp;lt;code&amp;gt;feedback_response_map&amp;lt;/code&amp;gt;) ===&lt;br /&gt;
&lt;br /&gt;
Produces the list of authors and the IDs of review responses that received&lt;br /&gt;
author feedback, bucketed by round for varying-rubric assignments.&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
def source&lt;br /&gt;
  Response&lt;br /&gt;
    .joins(:response_map)&lt;br /&gt;
    .where(&lt;br /&gt;
      response_maps: { type: 'ReviewResponseMap',&lt;br /&gt;
                       reviewed_object_id: @assignment.id }&lt;br /&gt;
    )&lt;br /&gt;
    .order(created_at: :desc)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
def grouper       = -&amp;gt;(r) { [r.map_id, r.round] }&lt;br /&gt;
def initial_state = { seen: Set.new, round_1: [],&lt;br /&gt;
                      round_2: [], round_3: [], all: [] }&lt;br /&gt;
&lt;br /&gt;
def accumulate(state, key, response)&lt;br /&gt;
  return if state[:seen].include?(key)&lt;br /&gt;
  state[:seen].add(key)&lt;br /&gt;
  if @assignment.varying_rubrics_by_round?&lt;br /&gt;
    case response.round&lt;br /&gt;
    when 1 then state[:round_1] &amp;lt;&amp;lt; response.id&lt;br /&gt;
    when 2 then state[:round_2] &amp;lt;&amp;lt; response.id&lt;br /&gt;
    when 3 then state[:round_3] &amp;lt;&amp;lt; response.id&lt;br /&gt;
    end&lt;br /&gt;
  else&lt;br /&gt;
    state[:all] &amp;lt;&amp;lt; response.id&lt;br /&gt;
  end&lt;br /&gt;
end&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Deduplication uses a &amp;lt;code&amp;gt;Set&amp;lt;/code&amp;gt;, which gives O(1) lookup, rather than the&lt;br /&gt;
array-based &amp;lt;code&amp;gt;seen_map_round_keys.include?&amp;lt;/code&amp;gt; from Repo X, which gives&lt;br /&gt;
O(n) lookup.&lt;br /&gt;
&lt;br /&gt;
Authors are fetched once in &amp;lt;code&amp;gt;finalize&amp;lt;/code&amp;gt;, not inside the stream.&lt;br /&gt;
&lt;br /&gt;
==== End-to-End Execution Flow ====&lt;br /&gt;
&lt;br /&gt;
The &amp;lt;code&amp;gt;run&amp;lt;/code&amp;gt; method is '''inherited from &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt;'''.&lt;br /&gt;
&amp;lt;code&amp;gt;FeedbackReport&amp;lt;/code&amp;gt; never defines it.&lt;br /&gt;
&lt;br /&gt;
Calling &amp;lt;code&amp;gt;FeedbackReport.new(assignment).run&amp;lt;/code&amp;gt; triggers the following&lt;br /&gt;
sequence:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
ReportsController&lt;br /&gt;
  REPORT_CLASSES['feedback_response_map'].new(assignment).run&lt;br /&gt;
    |&lt;br /&gt;
    |  (inherited from BaseReport)&lt;br /&gt;
    v&lt;br /&gt;
  BaseReport#run&lt;br /&gt;
    |&lt;br /&gt;
    |-- Step 1: state = initial_state&lt;br /&gt;
    |       =&amp;gt; { seen: Set.new,&lt;br /&gt;
    |            round_1: [], round_2: [], round_3: [], all: [] }&lt;br /&gt;
    |&lt;br /&gt;
    |-- Step 2: source.find_each(batch_size: 500)&lt;br /&gt;
    |       =&amp;gt; Response.joins(:response_map)&lt;br /&gt;
    |                  .where(type: 'ReviewResponseMap',&lt;br /&gt;
    |                         reviewed_object_id: assignment.id)&lt;br /&gt;
    |                  .order(created_at: :desc)&lt;br /&gt;
    |          streams Response records newest-first, in batches&lt;br /&gt;
    |&lt;br /&gt;
    |-- Step 3: for each Response row:&lt;br /&gt;
    |       key = grouper.call(row)&lt;br /&gt;
    |           =&amp;gt; [row.map_id, row.round]   e.g. [42, 1]&lt;br /&gt;
    |&lt;br /&gt;
    |       accumulate(state, key, row)&lt;br /&gt;
    |           =&amp;gt; skip if state[:seen] already has [map_id, round]&lt;br /&gt;
    |              (keeps only the latest response per map per round&lt;br /&gt;
    |               because source is ordered newest-first)&lt;br /&gt;
    |           =&amp;gt; otherwise: add key to :seen, then bucket row.id:&lt;br /&gt;
    |                round == 1  =&amp;gt;  state[:round_1] &amp;lt;&amp;lt; row.id&lt;br /&gt;
    |                round == 2  =&amp;gt;  state[:round_2] &amp;lt;&amp;lt; row.id&lt;br /&gt;
    |                round == 3  =&amp;gt;  state[:round_3] &amp;lt;&amp;lt; row.id&lt;br /&gt;
    |                (or state[:all] if single-rubric assignment)&lt;br /&gt;
    |&lt;br /&gt;
    |-- Step 4: finalize(state)&lt;br /&gt;
            =&amp;gt; fetch_authors  (one query: teams -&amp;gt; users -&amp;gt; participants)&lt;br /&gt;
            =&amp;gt; if varying_rubrics_by_round?&lt;br /&gt;
                 return { authors: [...],&lt;br /&gt;
                          review_response_ids: {&lt;br /&gt;
                            round_1: [...], round_2: [...], round_3: [...] } }&lt;br /&gt;
               else&lt;br /&gt;
                 return { authors: [...],&lt;br /&gt;
                          review_response_ids: [...] }&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
The key point is that &amp;lt;code&amp;gt;FeedbackReport&amp;lt;/code&amp;gt; only defines the four pieces&lt;br /&gt;
the pipeline needs:&lt;br /&gt;
&lt;br /&gt;
* &amp;lt;code&amp;gt;source&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;grouper&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;initial_state&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;accumulate&amp;lt;/code&amp;gt;&lt;br /&gt;
* &amp;lt;code&amp;gt;finalize&amp;lt;/code&amp;gt;&lt;br /&gt;
&lt;br /&gt;
Ruby's inheritance mechanism means calling &amp;lt;code&amp;gt;.run&amp;lt;/code&amp;gt; on a&lt;br /&gt;
&amp;lt;code&amp;gt;FeedbackReport&amp;lt;/code&amp;gt; instance automatically executes&lt;br /&gt;
&amp;lt;code&amp;gt;BaseReport#run&amp;lt;/code&amp;gt;, which calls back into &amp;lt;code&amp;gt;FeedbackReport&amp;lt;/code&amp;gt;'s&lt;br /&gt;
implementations of those methods.&lt;br /&gt;
&lt;br /&gt;
==== Sample Response (varying rubrics) ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;json&amp;quot;&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;type&amp;quot;: &amp;quot;feedback_response_map&amp;quot;,&lt;br /&gt;
  &amp;quot;authors&amp;quot;: [{ &amp;quot;id&amp;quot;: 7, &amp;quot;name&amp;quot;: &amp;quot;bob&amp;quot;, &amp;quot;full_name&amp;quot;: &amp;quot;Bob Jones&amp;quot; }],&lt;br /&gt;
  &amp;quot;review_response_ids&amp;quot;: {&lt;br /&gt;
    &amp;quot;round_1&amp;quot;: [12, 15], &amp;quot;round_2&amp;quot;: [18], &amp;quot;round_3&amp;quot;: []&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Teammate Review Report (&amp;lt;code&amp;gt;teammate_review_response_map&amp;lt;/code&amp;gt;) ===&lt;br /&gt;
&lt;br /&gt;
Streams &amp;lt;code&amp;gt;TeammateReviewResponseMap&amp;lt;/code&amp;gt; records grouped by&lt;br /&gt;
&amp;lt;code&amp;gt;reviewer_id&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
The first occurrence per reviewer is kept using deduplication via early return&lt;br /&gt;
if the key already exists in state. Reviewer associations are eagerly loaded.&lt;br /&gt;
&lt;br /&gt;
==== Sample Response ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;json&amp;quot;&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;type&amp;quot;: &amp;quot;teammate_review_response_map&amp;quot;,&lt;br /&gt;
  &amp;quot;reviewers&amp;quot;: [&lt;br /&gt;
    { &amp;quot;reviewer_id&amp;quot;: 5, &amp;quot;user_id&amp;quot;: 2,&lt;br /&gt;
      &amp;quot;name&amp;quot;: &amp;quot;alice&amp;quot;, &amp;quot;full_name&amp;quot;: &amp;quot;Alice Smith&amp;quot; }&lt;br /&gt;
  ]&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Bookmark Rating Report (&amp;lt;code&amp;gt;bookmark_rating_response_map&amp;lt;/code&amp;gt;) ===&lt;br /&gt;
&lt;br /&gt;
Streams &amp;lt;code&amp;gt;BookmarkRatingResponseMap&amp;lt;/code&amp;gt; records, accumulating distinct&lt;br /&gt;
bookmark IDs into a &amp;lt;code&amp;gt;Set&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Project topics are fetched once in &amp;lt;code&amp;gt;finalize&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
==== Bug Fixed During Port ====&lt;br /&gt;
&lt;br /&gt;
The model's &amp;lt;code&amp;gt;bookmark_response_report&amp;lt;/code&amp;gt; in Repo Y was incorrectly&lt;br /&gt;
calling:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
.pluck(:reviewed_object_id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
This returns assignment IDs, since &amp;lt;code&amp;gt;reviewed_object_id&amp;lt;/code&amp;gt; is the&lt;br /&gt;
foreign key to &amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
Bookmark IDs are stored in &amp;lt;code&amp;gt;reviewee_id&amp;lt;/code&amp;gt;. Therefore, this was fixed&lt;br /&gt;
to:&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;ruby&amp;quot;&amp;gt;&lt;br /&gt;
.pluck(:reviewee_id)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
==== Sample Response ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;json&amp;quot;&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;type&amp;quot;: &amp;quot;bookmark_rating_response_map&amp;quot;,&lt;br /&gt;
  &amp;quot;bookmark_ids&amp;quot;: [10, 14, 22],&lt;br /&gt;
  &amp;quot;topics&amp;quot;: [{ &amp;quot;id&amp;quot;: 3, &amp;quot;topic_name&amp;quot;: &amp;quot;Machine Learning&amp;quot; }]&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
=== Basic Report (&amp;lt;code&amp;gt;basic&amp;lt;/code&amp;gt;) ===&lt;br /&gt;
&lt;br /&gt;
Returns minimal assignment metadata.&lt;br /&gt;
&lt;br /&gt;
No streaming is required since all data comes from the already-loaded&lt;br /&gt;
&amp;lt;code&amp;gt;Assignment&amp;lt;/code&amp;gt; object.&lt;br /&gt;
&lt;br /&gt;
This report is used as the default when no type parameter is provided.&lt;br /&gt;
&lt;br /&gt;
==== Sample Response ====&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;json&amp;quot;&amp;gt;&lt;br /&gt;
{&lt;br /&gt;
  &amp;quot;type&amp;quot;: &amp;quot;basic&amp;quot;,&lt;br /&gt;
  &amp;quot;assignment_id&amp;quot;: 1,&lt;br /&gt;
  &amp;quot;assignment&amp;quot;: {&lt;br /&gt;
    &amp;quot;id&amp;quot;: 1, &amp;quot;name&amp;quot;: &amp;quot;Project 1&amp;quot;,&lt;br /&gt;
    &amp;quot;num_review_rounds&amp;quot;: 2,&lt;br /&gt;
    &amp;quot;varying_rubrics_by_round&amp;quot;: true&lt;br /&gt;
  }&lt;br /&gt;
}&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== File Structure ==&lt;br /&gt;
&lt;br /&gt;
&amp;lt;syntaxhighlight lang=&amp;quot;text&amp;quot;&amp;gt;&lt;br /&gt;
app/&lt;br /&gt;
  controllers/&lt;br /&gt;
    reports_controller.rb        Entry point, REPORT_CLASSES dispatch&lt;br /&gt;
  helpers/&lt;br /&gt;
    report_formatter_helper.rb   Empty namespace (logic moved to services)&lt;br /&gt;
  services/&lt;br /&gt;
    reports/&lt;br /&gt;
      base_report.rb             Abstract pipeline template&lt;br /&gt;
      review_report.rb           3-pipeline coordinator&lt;br /&gt;
      feedback_report.rb         Single pipeline, round bucketing&lt;br /&gt;
      teammate_review_report.rb  Single pipeline&lt;br /&gt;
      bookmark_rating_report.rb  Single pipeline&lt;br /&gt;
      basic_report.rb            Simple struct&lt;br /&gt;
  models/&lt;br /&gt;
    review_response_map.rb&lt;br /&gt;
      .review_response_report class method&lt;br /&gt;
    feedback_response_map.rb&lt;br /&gt;
      .feedback_response_report class method&lt;br /&gt;
    teammate_review_response_map.rb&lt;br /&gt;
      .teammate_response_report class method&lt;br /&gt;
    bookmark_rating_response_map.rb&lt;br /&gt;
      .bookmark_response_report (bug fixed)&lt;br /&gt;
&amp;lt;/syntaxhighlight&amp;gt;&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Blocked Report Types ==&lt;br /&gt;
&lt;br /&gt;
The following report types exist in Repo X but cannot yet be implemented in&lt;br /&gt;
Repo Y due to missing database tables or models.&lt;br /&gt;
&lt;br /&gt;
Each is ready to be added once its dependency is ported.&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Report Type !! Missing Dependency !! Repo X Location&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;calibration&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;calibrate_to&amp;lt;/code&amp;gt; column on &amp;lt;code&amp;gt;response_maps&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;report_formatter_helper.rb&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;self_review&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;SelfReviewResponseMap&amp;lt;/code&amp;gt; model&lt;br /&gt;
| &amp;lt;code&amp;gt;self_review_response_map.rb&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;survey&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;survey_deployments&amp;lt;/code&amp;gt; table&lt;br /&gt;
| &amp;lt;code&amp;gt;survey_response_map.rb&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;quiz&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;quiz_responses&amp;lt;/code&amp;gt; table&lt;br /&gt;
| &amp;lt;code&amp;gt;quiz_response_map.rb&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| &amp;lt;code&amp;gt;answer_tagging&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;tag_prompt_deployments&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;answer_tags&amp;lt;/code&amp;gt; tables&lt;br /&gt;
| &amp;lt;code&amp;gt;tag_prompt_deployment.rb&amp;lt;/code&amp;gt;&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
To add a blocked report once its dependencies are available:&lt;br /&gt;
&lt;br /&gt;
# Create &amp;lt;code&amp;gt;app/services/reports/&amp;amp;lt;name&amp;amp;gt;_report.rb&amp;lt;/code&amp;gt; inheriting &amp;lt;code&amp;gt;BaseReport&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Define &amp;lt;code&amp;gt;source&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;grouper&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;initial_state&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;accumulate&amp;lt;/code&amp;gt;, and &amp;lt;code&amp;gt;finalize&amp;lt;/code&amp;gt;.&lt;br /&gt;
# Add an entry to &amp;lt;code&amp;gt;ReportsController::REPORT_CLASSES&amp;lt;/code&amp;gt;.&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Comparison with Repo X ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Concern !! Repo X !! Repo Y&lt;br /&gt;
|-&lt;br /&gt;
| Output format&lt;br /&gt;
| ERB instance variables, such as &amp;lt;code&amp;gt;@reviewers&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;@review_scores&amp;lt;/code&amp;gt;&lt;br /&gt;
| JSON hash from &amp;lt;code&amp;gt;report.run&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Loading strategy&lt;br /&gt;
| All records loaded into arrays at once&lt;br /&gt;
| &amp;lt;code&amp;gt;find_each&amp;lt;/code&amp;gt; batched streaming&lt;br /&gt;
|-&lt;br /&gt;
| Metrics location&lt;br /&gt;
| &amp;lt;code&amp;gt;compute_metrics&amp;lt;/code&amp;gt; in helper base&lt;br /&gt;
| Each report owns &amp;lt;code&amp;gt;accumulate&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;finalize&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| Dispatch&lt;br /&gt;
| &amp;lt;code&amp;gt;send(@type.underscore, params, session)&amp;lt;/code&amp;gt;&lt;br /&gt;
| &amp;lt;code&amp;gt;REPORT_CLASSES[type].new(assignment).run&amp;lt;/code&amp;gt;&lt;br /&gt;
|-&lt;br /&gt;
| N+1 on scores&lt;br /&gt;
| &amp;lt;code&amp;gt;response.maximum_score&amp;lt;/code&amp;gt; per row — questionnaire lookup each time&lt;br /&gt;
| Precomputed &amp;lt;code&amp;gt;round-&amp;gt;max_score&amp;lt;/code&amp;gt; map, one query before pipeline&lt;br /&gt;
|-&lt;br /&gt;
| Deduplication&lt;br /&gt;
| &amp;lt;code&amp;gt;Array#include?&amp;lt;/code&amp;gt; — O(n) per check&lt;br /&gt;
| &amp;lt;code&amp;gt;Set#include?&amp;lt;/code&amp;gt; — O(1) per check&lt;br /&gt;
|-&lt;br /&gt;
| Additional features&lt;br /&gt;
| LLM evaluation, CSV export, calibration, self-review, survey, quiz, answer tagging&lt;br /&gt;
| Not yet ported; blocked on schema&lt;br /&gt;
|}&lt;br /&gt;
&lt;br /&gt;
----&lt;br /&gt;
&lt;br /&gt;
== Author ==&lt;br /&gt;
&lt;br /&gt;
{| class=&amp;quot;wikitable&amp;quot;&lt;br /&gt;
! Name !! Role&lt;br /&gt;
|-&lt;br /&gt;
| '''Aanand Sreekumaran Nair Jayakumari'''&lt;br /&gt;
| Project contributor / developer&lt;br /&gt;
|}&lt;/div&gt;</description>
			<pubDate>Sat, 02 May 2026 21:49:32 GMT</pubDate>
			<dc:creator>Asreeku</dc:creator>
			<comments>https://wiki.expertiza.ncsu.edu/index.php?title=Talk:Report_generation_framework(WIP)</comments>
		</item>
		<item>
			<title>Main Page</title>
			<link>https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page&amp;diff=168196&amp;oldid=168193</link>
			<guid isPermaLink="false">https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page&amp;diff=168196&amp;oldid=168193</guid>
			<description>&lt;p&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #fff; color: #202122;&quot; data-mw=&quot;interface&quot;&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;tr class=&quot;diff-title&quot; lang=&quot;en&quot;&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;← Older revision&lt;/td&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;Revision as of 21:43, 2 May 2026&lt;/td&gt;
				&lt;/tr&gt;&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l41&quot;&gt;Line 41:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 41:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[ReactJs Frontend]]&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[ReactJs Frontend]]&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[Front-End/Back-End]]&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[Front-End/Back-End]]&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[Report generation framework]]&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[Report generation framework&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;(WIP)&lt;/ins&gt;]]&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;==Application Behavior==&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;==Application Behavior==&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;</description>
			<pubDate>Sat, 02 May 2026 21:43:54 GMT</pubDate>
			<dc:creator>Asreeku</dc:creator>
			<comments>https://wiki.expertiza.ncsu.edu/index.php?title=Talk:Main_Page</comments>
		</item>
		<item>
			<title>Report generation framework</title>
			<link>https://wiki.expertiza.ncsu.edu/index.php?title=Report_generation_framework&amp;diff=168195&amp;oldid=0</link>
			<guid isPermaLink="false">https://wiki.expertiza.ncsu.edu/index.php?title=Report_generation_framework&amp;diff=168195&amp;oldid=0</guid>
			<description>&lt;p&gt;Created page with &amp;quot;# Report Generation&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;# Report Generation&lt;/div&gt;</description>
			<pubDate>Sat, 02 May 2026 21:43:05 GMT</pubDate>
			<dc:creator>Asreeku</dc:creator>
			<comments>https://wiki.expertiza.ncsu.edu/index.php?title=Talk:Report_generation_framework</comments>
		</item>
		<item>
			<title>CSC/ECE 517 Spring 2026 - E2612. Course-based reports</title>
			<link>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2612._Course-based_reports&amp;diff=168194&amp;oldid=168192</link>
			<guid isPermaLink="false">https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2612._Course-based_reports&amp;diff=168194&amp;oldid=168192</guid>
			<description>&lt;p&gt;&lt;span dir=&quot;auto&quot;&gt;&lt;span class=&quot;autocomment&quot;&gt;8. Reuse Decisions&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #fff; color: #202122;&quot; data-mw=&quot;interface&quot;&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;tr class=&quot;diff-title&quot; lang=&quot;en&quot;&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;← Older revision&lt;/td&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;Revision as of 21:34, 2 May 2026&lt;/td&gt;
				&lt;/tr&gt;&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l298&quot;&gt;Line 298:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 298:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;| Legacy Rails report views || Not reused || Clean reimplementation only&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;| Legacy Rails report views || Not reused || Clean reimplementation only&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;|}&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;|}&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;The project was designed to be minimally intrusive by reusing existing, well-tested code whenever possible instead of rebuilding functionality from scratch. The biggest reuse decision was the shared Table.tsx component, whose TanStack Table foundation already supported grouped headers, sorting, and column visibility, covering most of the report’s interactive needs with only a small addition: disablePaginationRowModel. We also followed the existing patterns in gradesService.ts and Course.tsx for the service and routing layers, keeping the new code consistent and easy to understand within the existing codebase.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;== 9. Tests ==&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;== 9. Tests ==&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;</description>
			<pubDate>Sat, 02 May 2026 21:34:13 GMT</pubDate>
			<dc:creator>Rsgopara</dc:creator>
			<comments>https://wiki.expertiza.ncsu.edu/index.php?title=Talk:CSC/ECE_517_Spring_2026_-_E2612._Course-based_reports</comments>
		</item>
		<item>
			<title>Main Page</title>
			<link>https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page&amp;diff=168193&amp;oldid=167504</link>
			<guid isPermaLink="false">https://wiki.expertiza.ncsu.edu/index.php?title=Main_Page&amp;diff=168193&amp;oldid=167504</guid>
			<description>&lt;p&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #fff; color: #202122;&quot; data-mw=&quot;interface&quot;&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;tr class=&quot;diff-title&quot; lang=&quot;en&quot;&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;← Older revision&lt;/td&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;Revision as of 21:30, 2 May 2026&lt;/td&gt;
				&lt;/tr&gt;&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l41&quot;&gt;Line 41:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 41:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[ReactJs Frontend]]&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[ReactJs Frontend]]&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[Front-End/Back-End]]&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;* [[Front-End/Back-End]]&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;* [[Report generation framework]]&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;==Application Behavior==&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;==Application Behavior==&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;</description>
			<pubDate>Sat, 02 May 2026 21:30:20 GMT</pubDate>
			<dc:creator>Asreeku</dc:creator>
			<comments>https://wiki.expertiza.ncsu.edu/index.php?title=Talk:Main_Page</comments>
		</item>
		<item>
			<title>CSC/ECE 517 Spring 2026 - E2612. Course-based reports</title>
			<link>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2612._Course-based_reports&amp;diff=168192&amp;oldid=168186</link>
			<guid isPermaLink="false">https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2612._Course-based_reports&amp;diff=168192&amp;oldid=168186</guid>
			<description>&lt;p&gt;&lt;span dir=&quot;auto&quot;&gt;&lt;span class=&quot;autocomment&quot;&gt;6.2 Response Shape&lt;/span&gt;&lt;/span&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #fff; color: #202122;&quot; data-mw=&quot;interface&quot;&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;tr class=&quot;diff-title&quot; lang=&quot;en&quot;&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;← Older revision&lt;/td&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;Revision as of 21:28, 2 May 2026&lt;/td&gt;
				&lt;/tr&gt;&lt;tr&gt;&lt;td colspan=&quot;4&quot; class=&quot;diff-multi&quot; lang=&quot;en&quot;&gt;(5 intermediate revisions by the same user not shown)&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l25&quot;&gt;Line 25:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 25:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;This project consolidates both into a single unified table, accessible from the Courses list page. The table displays every enrolled student alongside their grades, topics, teammate scores, and author feedback scores for every assignment in the course, with instructor-controlled column visibility to keep the view manageable. The implementation targets Expertiza's reimplemented React/TypeScript frontend. The legacy Rails views serve only as a reference for the underlying data model; no legacy code was reused.&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;This project consolidates both into a single unified table, accessible from the Courses list page. The table displays every enrolled student alongside their grades, topics, teammate scores, and author feedback scores for every assignment in the course, with instructor-controlled column visibility to keep the view manageable. The implementation targets Expertiza's reimplemented React/TypeScript frontend. The legacy Rails views serve only as a reference for the underlying data model; no legacy code was reused.&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;[[File:E2612-FinalFrontend1.png|1100px]]&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;== 2. Requirements ==&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;== 2. Requirements ==&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l143&quot;&gt;Line 143:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 145:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;=== 6.2 Response Shape ===&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;=== 6.2 Response Shape ===&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&amp;lt;&lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;pre&lt;/del&gt;&amp;gt;&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&amp;lt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;syntaxhighlight lang=&quot;ruby&quot;&lt;/ins&gt;&amp;gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;{&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;{&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;   &amp;quot;course_id&amp;quot;: 44,&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;   &amp;quot;course_id&amp;quot;: 44,&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l183&quot;&gt;Line 183:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 185:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;   ]&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;   ]&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;}&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;}&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&amp;lt;/&lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;pre&lt;/del&gt;&amp;gt;&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&amp;lt;/&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;syntaxhighlight&lt;/ins&gt;&amp;gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;Assignments are sorted by their final review deadline before building the report. The &amp;lt;code&amp;gt;topic&amp;lt;/code&amp;gt; field is present only when &amp;lt;code&amp;gt;has_topics&amp;lt;/code&amp;gt; is true for that assignment.&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;Assignments are sorted by their final review deadline before building the report. The &amp;lt;code&amp;gt;topic&amp;lt;/code&amp;gt; field is present only when &amp;lt;code&amp;gt;has_topics&amp;lt;/code&amp;gt; is true for that assignment.&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l236&quot;&gt;Line 236:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 238:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;|}&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;|}&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;=== 7.3 &lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Report Page Screenshot &lt;/del&gt;===&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;=== 7.3 &lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Frontend Type Definitions &lt;/ins&gt;===&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;[[File:E2612-FinalFrontend1.png|800px]]&lt;/del&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&amp;lt;syntaxhighlight lang&lt;/ins&gt;=&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&quot;ruby&quot;&lt;/ins&gt;&amp;gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-added&quot;&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;==&lt;/del&gt;= &lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;7.4 Frontend Type Definitions ===&lt;/del&gt;&lt;/div&gt;&lt;/td&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-added&quot;&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-added&quot;&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&amp;lt;pre&lt;/del&gt;&amp;gt;&lt;/div&gt;&lt;/td&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-added&quot;&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;interface AssignmentMetadata {&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;interface AssignmentMetadata {&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;   assignment_id: number;&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;   assignment_id: number;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l278&quot;&gt;Line 278:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 276:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;   avgAuthorFeedbackScore: boolean;&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;   avgAuthorFeedbackScore: boolean;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;};&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;};&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&amp;lt;/&lt;del style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;pre&lt;/del&gt;&amp;gt;&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&amp;lt;/&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;syntaxhighlight&lt;/ins&gt;&amp;gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;== 8. Reuse Decisions ==&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;== 8. Reuse Decisions ==&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;</description>
			<pubDate>Sat, 02 May 2026 21:28:12 GMT</pubDate>
			<dc:creator>Rsgopara</dc:creator>
			<comments>https://wiki.expertiza.ncsu.edu/index.php?title=Talk:CSC/ECE_517_Spring_2026_-_E2612._Course-based_reports</comments>
		</item>
		<item>
			<title>CSC/ECE 517 Spring 2026 - E2612. Course-based reports</title>
			<link>https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2612._Course-based_reports&amp;diff=168186&amp;oldid=168171</link>
			<guid isPermaLink="false">https://wiki.expertiza.ncsu.edu/index.php?title=CSC/ECE_517_Spring_2026_-_E2612._Course-based_reports&amp;diff=168186&amp;oldid=168171</guid>
			<description>&lt;p&gt;&lt;/p&gt;
&lt;table style=&quot;background-color: #fff; color: #202122;&quot; data-mw=&quot;interface&quot;&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;col class=&quot;diff-marker&quot; /&gt;
				&lt;col class=&quot;diff-content&quot; /&gt;
				&lt;tr class=&quot;diff-title&quot; lang=&quot;en&quot;&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;← Older revision&lt;/td&gt;
				&lt;td colspan=&quot;2&quot; style=&quot;background-color: #fff; color: #202122; text-align: center;&quot;&gt;Revision as of 19:19, 2 May 2026&lt;/td&gt;
				&lt;/tr&gt;&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot; id=&quot;mw-diff-left-l301&quot;&gt;Line 301:&lt;/td&gt;
&lt;td colspan=&quot;2&quot; class=&quot;diff-lineno&quot;&gt;Line 301:&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;|}&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;|}&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;−&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #ffe49c; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;== 9. Out of Scope ==&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;== 9&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;. Tests ==&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;'''Frontend'''&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;The main page test is CourseReport.test.tsx (line 98). It mocks &amp;lt;code&amp;gt;useParams&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;useAPI&amp;lt;/code&amp;gt;, and the shared &amp;lt;code&amp;gt;Table&amp;lt;/code&amp;gt; component, then verifies:&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Fetches &amp;lt;code&amp;gt;/course_reports?course_id=44&amp;lt;/code&amp;gt; when &amp;lt;code&amp;gt;courseId&amp;lt;/code&amp;gt; exists, and skips the request when missing.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Shows loading spinner and error alert states without rendering tables.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Renders the title as &amp;lt;code&amp;gt;Course Report — CSC 517&amp;lt;/code&amp;gt;.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Renders all field toggles checked by default: Topic, Peer Grade, Instructor Grade, Avg. Teammate Score, Avg. Author Feedback Score.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Splits data into two tables: first table gets only &amp;lt;code&amp;gt;Class Average&amp;lt;/code&amp;gt;, second table gets student rows.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Disables pagination/global filtering on both tables.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Updates column visibility when each checkbox is toggled.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;The transformation/service tests are in courseAssignmentOverviewService.test.ts (line 92). They cover:&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&amp;lt;code&amp;gt;buildRows&amp;lt;/code&amp;gt;: one row per student plus a class average row.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Dynamic keys like &amp;lt;code&amp;gt;a3_peerGrade&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;a3_instructorGrade&amp;lt;/code&amp;gt;, etc.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Topic fields only for topic-enabled assignments.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Missing assignment participation is skipped.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Student &amp;lt;code&amp;gt;null&amp;lt;/code&amp;gt; scores remain &amp;lt;code&amp;gt;null&amp;lt;/code&amp;gt;.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Class average treats &amp;lt;code&amp;gt;null&amp;lt;/code&amp;gt; numeric scores as &amp;lt;code&amp;gt;0&amp;lt;/code&amp;gt;.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Empty student lists still produce a &amp;lt;code&amp;gt;Class Average&amp;lt;/code&amp;gt; row.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&amp;lt;code&amp;gt;buildColumns&amp;lt;/code&amp;gt;: student column first, assignment groups, sortable subcolumns, visibility metadata, and bold rendering for class average cells.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;There are also adjacent course navigation tests:&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Course.test.tsx (line 107) checks that &amp;lt;code&amp;gt;/courses/1/class_assignment_overview&amp;lt;/code&amp;gt; renders only the nested outlet, not the normal course table.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;CourseColumns.test.tsx (line 17) checks the “View Course Report” action button, tooltip, click handler, and styling.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;'''Backend'''&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;The backend request spec is course_reports_controller_spec.rb (line 313). It builds a fairly rich dataset with a course, two in-course assignments, one outside-course assignment, teams, participants, review responses, feedback responses, teammate reviews, topics, and due dates.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;The successful &amp;lt;code&amp;gt;200&amp;lt;/code&amp;gt; case validates:&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Response shape: &amp;lt;code&amp;gt;course_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;course_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;assignments&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;students&amp;lt;/code&amp;gt;.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Assignment metadata includes &amp;lt;code&amp;gt;assignment_id&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;assignment_name&amp;lt;/code&amp;gt;, &amp;lt;code&amp;gt;has_topics&amp;lt;/code&amp;gt;.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Assignments are ordered by final review deadline.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Topic-enabled assignments include &amp;lt;code&amp;gt;topic&amp;lt;/code&amp;gt;; non-topic assignments do not.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Outside-course assignments are excluded.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Student rows include expected assignment entries keyed by assignment id.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;Calculated fields are correct for multiple participant situations:&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&amp;lt;code&amp;gt;peer_grade&amp;lt;/code&amp;gt;&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&amp;lt;code&amp;gt;instructor_grade&amp;lt;/code&amp;gt;&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&amp;lt;code&amp;gt;avg_teammate_score&amp;lt;/code&amp;gt;&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&amp;lt;code&amp;gt;avg_author_feedback_score&amp;lt;/code&amp;gt;&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&amp;lt;code&amp;gt;topic&amp;lt;/code&amp;gt;&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;It also covers error/auth cases:&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&amp;lt;code&amp;gt;404&amp;lt;/code&amp;gt; when the course is missing.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&amp;lt;code&amp;gt;500&amp;lt;/code&amp;gt; when an assignment’s final due date is not a review deadline.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; for students.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&amp;lt;code&amp;gt;403&amp;lt;/code&amp;gt; for instructors outside the course teaching staff.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&amp;lt;code&amp;gt;401&amp;lt;/code&amp;gt; for invalid tokens.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;&amp;lt;code&amp;gt;401&amp;lt;/code&amp;gt; when no bearer token is provided.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;In short: backend tests prove the API contract and score calculations from database records; frontend tests prove the page fetches that contract, transforms it into dynamic report rows/columns, and gives users the expected table/toggle behavior.&lt;/ins&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt; &lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td colspan=&quot;2&quot; class=&quot;diff-side-deleted&quot;&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot; data-marker=&quot;+&quot;&gt;&lt;/td&gt;&lt;td style=&quot;color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #a3d3ff; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;&lt;ins style=&quot;font-weight: bold; text-decoration: none;&quot;&gt;== 10&lt;/ins&gt;. Out of Scope ==&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;br&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;The following are not part of this implementation:&lt;/div&gt;&lt;/td&gt;&lt;td class=&quot;diff-marker&quot;&gt;&lt;/td&gt;&lt;td style=&quot;background-color: #f8f9fa; color: #202122; font-size: 88%; border-style: solid; border-width: 1px 1px 1px 4px; border-radius: 0.33em; border-color: #eaecf0; vertical-align: top; white-space: pre-wrap;&quot;&gt;&lt;div&gt;The following are not part of this implementation:&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;
&lt;/table&gt;</description>
			<pubDate>Sat, 02 May 2026 19:19:07 GMT</pubDate>
			<dc:creator>Mskamat</dc:creator>
			<comments>https://wiki.expertiza.ncsu.edu/index.php?title=Talk:CSC/ECE_517_Spring_2026_-_E2612._Course-based_reports</comments>
		</item>
</channel></rss>