<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.expertiza.ncsu.edu/index.php?action=history&amp;feed=atom&amp;title=Report_generation_framework%28WIP%29</id>
	<title>Report generation framework(WIP) - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.expertiza.ncsu.edu/index.php?action=history&amp;feed=atom&amp;title=Report_generation_framework%28WIP%29"/>
	<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Report_generation_framework(WIP)&amp;action=history"/>
	<updated>2026-06-25T00:42:16Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.41.0</generator>
	<entry>
		<id>https://wiki.expertiza.ncsu.edu/index.php?title=Report_generation_framework(WIP)&amp;diff=168197&amp;oldid=prev</id>
		<title>Asreeku: Created page with &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 '''Repo X''') into the reimplemented Rails API back-end (referred to as '''Repo Y''') and redesigned in the process.  Repo X used a Rails helper module (&lt;code&gt;ReportFormatterHelper&lt;/code&gt;) that assigned instance variables, such as &lt;code&gt;@reviewers&lt;/cod...&quot;</title>
		<link rel="alternate" type="text/html" href="https://wiki.expertiza.ncsu.edu/index.php?title=Report_generation_framework(WIP)&amp;diff=168197&amp;oldid=prev"/>
		<updated>2026-05-02T21:49:32Z</updated>

		<summary type="html">&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;</summary>
		<author><name>Asreeku</name></author>
	</entry>
</feed>