Skip to content

Pipeline for report generation#340

Open
aanand-1706 wants to merge 23 commits into
expertiza:mainfrom
aanand-1706:report-gen
Open

Pipeline for report generation#340
aanand-1706 wants to merge 23 commits into
expertiza:mainfrom
aanand-1706:report-gen

Conversation

@aanand-1706

@aanand-1706 aanand-1706 commented Apr 27, 2026

Copy link
Copy Markdown
Contributor

Ports feedback_response_map report from Expertiza (repo X) to the JSON API architecture of repo Y using a streaming reduce pipeline.

  • FeedbackResponseMap: adds feedback_response_report class method (deduplicates responses per map/round, buckets by round for varying-rubric assignments)
  • BaseReport: new abstract pipeline base (source -> grouper ->
    accumulate -> finalize via find_each streaming)
  • FeedbackReport: concrete pipeline subclass; uses Set for O(1) deduplication; fetches authors once in finalize
  • ReportsController: replaces send(type) dispatch with REPORT_CLASSES hash; renders JSON from report.run
  • ReportFormatterHelper: emptied (logic moved to services)

Danger Policy Violations & Code Quality Issues

🔴 Critical Issues

Missing Database Migration for New Model

  • The PR introduces a new AnswerTag model (app/models/answer_tag.rb) with associations to answer, tag_prompt_deployment, and user, plus presence validations for all required fields
  • No migration file exists to create the answer_tags table — confirmed by absence from db/schema.rb table list
  • This violates Expertiza schema policy: model files without corresponding migrations cause runtime failures when code attempts to persist records
  • The migration must be added before this PR can be merged

Unresolved TODO Marker in Production Code

  • app/models/reports/answer_tagging_report.rb contains a TODO comment: "confirm with prof — if a reviewer submits multiple responses for the same round, only the latest submitted response should be counted as taggable"
  • This represents incomplete design requiring external approval that remains committed to code
  • Indicates unresolved questions about business logic correctness and suggests the feature may not be fully specced

🟠 High Severity Issues

Zero Test Coverage for Major Architectural Refactoring

  • The PR adds ~600+ lines of code introducing a new streaming reduce pipeline pattern:
    • New ReportsController with fetch_response_report action and REPORT_CLASSES factory mapping
    • New abstract base class Reports::BaseReport implementing streaming reducer pattern
    • Five concrete report classes: FeedbackReport, ReviewReport, TeammateReviewReport, BookmarkRatingReport, AnswerTaggingReport
    • Multiple nested reducer classes with complex state accumulation logic
  • No spec or test files were added for the controller action, base class, or any concrete report implementations
  • Complex streaming/reducing patterns with state management introduced without test validation

Duplicate File Implementations

  • Two versions of feedback_report.rb exist with differing implementations:
    • app/services/reports/feedback_report.rb (uses @assignment)
    • app/models/reports/feedback_report.rb (uses @reportable)
  • This indicates incomplete refactoring and creates maintenance burden; code review must verify which is authoritative

🟡 Medium Severity Issues

Large Refactoring Scope Without Isolated Testing

  • PR simultaneously refactors multiple architectural layers:
    • Changes ReportsController from dynamic dispatch to factory pattern
    • Introduces new pipeline base class affecting five report types
    • Adds scopes to multiple models: Answer, Item, Response, ReviewResponseMap, TeamsUser, AssignmentQuestionnaire
    • Modifies routes and empties ReportFormatterHelper
  • All changes are interdependent and untested, making rollback difficult

Weak Error Handling in Controller

  • Catches generic StandardError and returns raw e.message to client (potential information disclosure)
  • No specific exception types for domain-specific conditions
  • Error response structure inconsistent

Incomplete Model Relationships

  • AnswerTag model references associations to tag_prompt_deployment and user
  • No verification that these models exist or foreign key validation is complete

ℹ️ Additional Observations

  • No vendor/dependency changes (good)
  • No debug statements found (good)
  • Routes configured with POST-only constraint (good)
  • New model scopes are well-defined (good)

Blocking Requirements Before Merge

  1. Create database migration to create answer_tags table with:

    • Columns: id, answer_id, tag_prompt_deployment_id, user_id, value, timestamps
    • Foreign key constraints and appropriate indexes
    • Run migration to update db/schema.rb
  2. Resolve TODO marker in answer_tagging_report.rb with explicit implementation decision (implement deduplication or document decision not to)

  3. Consolidate duplicate feedback_report.rb — verify which version is correct or document why both are needed

Recommended Test Coverage

  1. Add RSpec specs for ReportsController#fetch_response_report covering all report types, error cases, and permissions
  2. Add model specs for each Reports::* class covering pipeline methods and finalization
  3. Add specs for new model scopes and AnswerTag associations/validations

Review Change Stack

Ports feedback_response_map report from Expertiza (repo X) to the
JSON API architecture of repo Y using a streaming reduce pipeline.

- FeedbackResponseMap: adds feedback_response_report class method
  (deduplicates responses per map/round, buckets by round for
  varying-rubric assignments)
- BaseReport: new abstract pipeline base (source -> grouper ->
  accumulate -> finalize via find_each streaming)
- FeedbackReport: concrete pipeline subclass; uses Set for O(1)
  deduplication; fetches authors once in finalize
- ReportsController: replaces send(type) dispatch with
  REPORT_CLASSES hash; renders JSON from report.run
- ReportFormatterHelper: emptied (logic moved to services)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 27, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This pull request introduces a new streaming report pipeline infrastructure for the Expertiza backend. It adds data model scopes to support efficient querying, a base BaseReport scaffold for implementing reduce-style pipelines, and concrete report implementations for peer reviews, answer tagging, feedback, bookmarks, and teammate reviews. A new ReportsController exposes reports via JSON, and routing is updated to remove deprecated response submit/unsubmit endpoints.

Changes

Report System Implementations

Layer / File(s) Summary
Data access scopes and model foundations
app/models/Item.rb, app/models/answer_tag.rb, app/models/assignment_questionnaire.rb, app/models/teams_user.rb, app/models/answer.rb, app/models/review_response_map.rb, app/models/response.rb
New AnswerTag model with associations and validations; scopes added to Item, Answer, AssignmentQuestionnaire, TeamsUser, ReviewResponseMap, and Response to filter by assignment, questionnaire, item, response, and deployment context.
Base report streaming pipeline scaffold
app/models/reports/base_report.rb
BaseReport class implements a reusable streaming reduce pattern with factory methods, abstract extension points for source, state_key_for, initial_state, and accumulate, plus optional finalize override. Streams rows via find_each(batch_size: 500) and groups by key.
ReviewReport with three reducers
app/models/reports/review_report.rb
Peer-review report runs three reducers over shared state: ReviewersReducer populates reviewer metadata, ScoresReducer computes normalized score percentages per reviewer/reviewee/round, and AvgRangesReducer aggregates team-level average grades. Returns sorted reviewers with nested score data plus avg/ranges summary.
FeedbackReport for response deduplication
app/models/reports/feedback_report.rb
Collects latest response per (map, round) pair ordered newest-first, buckets IDs into per-round or unified lists based on rubric mode, and returns formatted author metadata with corresponding review response ID structure.
AnswerTaggingReport with deployment aggregation
app/models/reports/answer_tagging_report.rb
Two-phase report: DeploymentReducer runs nested TaggableAnswersReducer and TaggingStatsReducer per deployment to accumulate per-user tagged/not-tagged/taggable counts and tag update intervals; UserSummaryReducer merges stats across deployments and recomputes cross-deployment percentages.
BookmarkRatingReport and TeammateReviewReport
app/models/reports/bookmark_rating_report.rb, app/models/reports/teammate_review_report.rb
BookmarkRatingReport collects distinct bookmark IDs by reviewee with project topic metadata. TeammateReviewReport runs two sequential reducers to track reviewer identity, reviewee lists, and teammate count per reviewer.
ReportsController and routing
app/controllers/reports_controller.rb, config/routes.rb
New ReportsController with fetch_response_report action that looks up report type from registry, loads assignment, executes report pipeline, and returns JSON with error-to-HTTP-status mapping. Routes add POST /reports/fetch_response_report and remove deprecated responses/:id/submit and responses/:id/unsubmit endpoints.

🎯 4 (Complex) | ⏱️ ~60 minutes


Possibly related PRs

  • expertiza/reimplementation-back-end#331: Removes the responses submit/unsubmit member PATCH routes, which directly corresponds to the routes file changes in this PR that retire those same endpoints.

Suggested labels

api, database

🚥 Pre-merge checks | ✅ 9 | ❌ 5

❌ Failed checks (5 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.06% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Schema-Without-Migration ⚠️ Warning New AnswerTag ActiveRecord model introduced without migration file or schema.rb changes to create the answer_tags table. Add a migration file (db/migrate/TIMESTAMP_create_answer_tags.rb) to create the answer_tags table with columns for answer_id, tag_prompt_deployment_id, user_id, value, and timestamps.
Behavior-Change-Needs-Tests ⚠️ Warning PR adds ~740 lines: new ReportsController endpoint, AnswerTag model, BaseReport + 5 report classes, and scopes in 6 models—with zero test files added or modified. Add spec/models/answer_tag_spec.rb, spec/models/reports/*_spec.rb, spec/requests/api/v1/reports_controller_spec.rb, and update existing model specs for new scopes.
Todo-Temp-Debug-Artifacts ⚠️ Warning PR introduces a TODO marker in app/models/reports/answer_tagging_report.rb (line 94) indicating unresolved design work about deduplicating responses per reviewer/round. Remove the TODO comment and resolve the underlying design question about deduplication logic before merging, or document why the current implementation is acceptable.
Legacy-Pr-Scope-And-Title ⚠️ Warning PR adds 909 LoC across 17 files, exceeding the ~500 LoC threshold. Consider breaking into smaller, focused PRs for easier review. Break PR into multiple smaller changes: (1) BaseReport base class, (2) Individual report implementations, (3) Controller/routing changes. Target ~300-400 LoC per PR.
✅ Passed checks (9 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Pipeline for report generation' accurately describes the main change: introducing a streaming reduce pipeline architecture for report generation across multiple report classes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Workflow-Security ✅ Passed No GitHub Actions workflow files were modified in this PR; all changes are to Rails app code (controllers, models, helpers, routes), making this check not applicable.
Config-And-Setup-Scrutiny ✅ Passed No critical config files (database.yml, storage.yml, credentials.yml.enc, config.ru, setup.sh, workflow files) were modified in this PR; changes are limited to application code and config/routes.rb.
Legacy-Config-File-Guardrails ✅ Passed PR modifies only application code files (controllers, models, helpers, services, routes); no changes to legacy config files or guardrails patterns.
Legacy-Rspec-Hygiene ✅ Passed No spec files are added or modified in this PR; the legacy-rspec-hygiene check applies only to spec files, so no violations can be detected.
Legacy-Global-Debug-Code ✅ Passed No newly introduced global variables, class variables, or debugging statements (puts, print, binding.pry, debugger, console.log) found in any of the 16 PR files checked.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented Apr 27, 2026

Copy link
Copy Markdown
3 Warnings
⚠️ Your pull request is more than 500 LoC.
Please make sure you did not commit unnecessary changes, such as schema.rb, node_modules, or changelog noise.
⚠️ This pull request contains TODO or FIXME task(s); please fix them.
⚠️ There are code changes, but no corresponding tests.
Please include tests if this PR introduces any modifications in behavior.
1 Message
📖 Thanks for the pull request, and welcome! The Expertiza team is excited to review your changes, and you should hear from us soon.

Please make sure the PR passes all checks and you have run rubocop -a to autocorrect issues before requesting a review.

This repository is being automatically checked for code-quality issues using CodeRabbit, Danger, and CI workflows. Please address newly introduced issues before marking the PR ready for review.

If you have any questions, please send email to expertiza-support@lists.ncsu.edu.

Thanks for the pull request, and welcome! The Expertiza team is excited to review your changes, and you should hear from us soon.

Please make sure the PR passes all checks and you have run rubocop -a to autocorrect issues before requesting a review.

This repository is being automatically checked for code-quality issues using CodeRabbit, Danger, and CI workflows. Please address newly introduced issues before marking the PR ready for review.

If you have any questions, please send email to expertiza-support@lists.ncsu.edu.

Your pull request is more than 500 LoC.
Please make sure you did not commit unnecessary changes, such as schema.rb, node_modules, or changelog noise.

This pull request contains TODO or FIXME task(s); please fix them.

There are code changes, but no corresponding tests.
Please include tests if this PR introduces any modifications in behavior.

Generated by 🚫 Danger

@github-actions

Copy link
Copy Markdown

🚨 RSpec Tests Report

All tests passed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
app/services/reports/feedback_report.rb (1)

66-83: Author hydration is still N+1.

Line 70 does one AssignmentParticipant.find_by per team member, and Lines 81-82 can then hit p.user again per participant. On large assignments this turns finalize into a query-heavy path; load participants with includes(:user) in one query keyed by user_id instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/services/reports/feedback_report.rb` around lines 66 - 83, fetch_authors
currently does an AssignmentParticipant.find_by per team user and
format_participant then touches p.user causing N+1s; instead load all
participants with their users in one query and map them by user_id: in
fetch_authors collect team user ids from
AssignmentTeam.includes(:users).where(parent_id: `@assignment.id`), then query
AssignmentParticipant.includes(:user).where(parent_id: `@assignment.id`, user_id:
user_ids) and return those participant records (or a lookup keyed by user_id) so
format_participant can use the preloaded p.user without extra queries; update
fetch_authors and any callers to work with the returned participants/list.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/controllers/reports_controller.rb`:
- Around line 35-36: The rescue in ReportsController currently renders raw
e.message to clients; instead log the exception server-side (e.g., use
Rails.logger.error / logger.error with e.message and e.backtrace) and change the
response to render a generic JSON error like { error: "Internal server error" }
with status :internal_server_error; update the rescue StandardError => e block
to perform the logging first and then return the generic payload so internals
(SQL/class errors) are not exposed.

In `@app/models/feedback_response_map.rb`:
- Around line 27-71: The model method self.feedback_response_report duplicates
logic that belongs in Reports::FeedbackReport; remove the internal query/dedupe
logic in feedback_response_report and delegate to the canonical service
implementation in Reports::FeedbackReport, adapting any legacy return shape at
this boundary. Specifically, replace the body of
self.feedback_response_report(assignment_id) with a single call to the
Reports::FeedbackReport API (e.g., invoke the service class/method that computes
the authors and per-round or all_review_response_ids), and transform the service
result into the existing return tuple the caller expects (authors + round arrays
or authors + all_review_response_ids) so callers keep the same contract while
the service remains the single source of truth. Ensure you reference the same
semantics used by the service for varying_rubrics_by_round? and deduplication
(map_id vs map_id-round) when mapping the service output back to the model
method's return shape.

In `@app/services/reports/base_report.rb`:
- Around line 32-37: The use of ActiveRecord::Relation.find_each in
BaseReport#run ignores the custom ordering defined by FeedbackReport#source and
breaks the dedup logic in accumulate (which relies on "ordered newest-first");
update run to iterate while preserving order—either switch from find_each to the
Rails 8 cursor form (e.g., find_each(cursor: :created_at, order: :desc) or
equivalent) so the relation's order is respected, or replace the iteration by
selecting latest-per-(map, round) in SQL (push the "latest per group" logic into
the source query) and then iterate that ordered result; ensure the changes
reference the run method, source relation, and accumulate dedup behavior so
newest responses are seen first.

In `@app/services/reports/feedback_report.rb`:
- Around line 22-40: The deduplication key currently produced by the grouper
(->(r) { [r.map_id, r.round] }) causes multiple entries per map when
`@assignment.varying_rubrics_by_round`? is false; change the logic so the key used
for state[:seen] depends on the assignment: if varying_rubrics_by_round? then
keep [r.map_id, r.round], otherwise use r.map_id only. Update the grouper or
compute key inside accumulate (referencing grouper, accumulate,
varying_rubrics_by_round?, and initial_state) so that when rubrics do not vary
by round a map_id is deduped once and state[:all] only gets one response id per
map.

---

Nitpick comments:
In `@app/services/reports/feedback_report.rb`:
- Around line 66-83: fetch_authors currently does an
AssignmentParticipant.find_by per team user and format_participant then touches
p.user causing N+1s; instead load all participants with their users in one query
and map them by user_id: in fetch_authors collect team user ids from
AssignmentTeam.includes(:users).where(parent_id: `@assignment.id`), then query
AssignmentParticipant.includes(:user).where(parent_id: `@assignment.id`, user_id:
user_ids) and return those participant records (or a lookup keyed by user_id) so
format_participant can use the preloaded p.user without extra queries; update
fetch_authors and any callers to work with the returned participants/list.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 341c60ad-38b6-4404-9ea3-4c02238309e0

📥 Commits

Reviewing files that changed from the base of the PR and between cc03ecd and 6f32448.

📒 Files selected for processing (5)
  • app/controllers/reports_controller.rb
  • app/helpers/report_formatter_helper.rb
  • app/models/feedback_response_map.rb
  • app/services/reports/base_report.rb
  • app/services/reports/feedback_report.rb

Comment on lines +35 to +36
rescue StandardError => e
render json: { error: e.message }, status: :internal_server_error

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not return raw exception messages from the API.

Line 36 exposes e.message verbatim, which can leak SQL errors, class names, or other internals to callers. Log the exception server-side and return a generic 500 payload instead.

Proposed fix
   rescue ActiveRecord::RecordNotFound
     render json: { error: 'Assignment not found' }, status: :not_found
   rescue StandardError => e
-    render json: { error: e.message }, status: :internal_server_error
+    Rails.logger.error("[ReportsController#response_report] #{e.class}: #{e.message}")
+    render json: { error: 'Internal server error' }, status: :internal_server_error
   end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rescue StandardError => e
render json: { error: e.message }, status: :internal_server_error
rescue StandardError => e
Rails.logger.error("[ReportsController#response_report] #{e.class}: #{e.message}")
render json: { error: 'Internal server error' }, status: :internal_server_error
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/controllers/reports_controller.rb` around lines 35 - 36, The rescue in
ReportsController currently renders raw e.message to clients; instead log the
exception server-side (e.g., use Rails.logger.error / logger.error with
e.message and e.backtrace) and change the response to render a generic JSON
error like { error: "Internal server error" } with status
:internal_server_error; update the rescue StandardError => e block to perform
the logging first and then return the generic payload so internals (SQL/class
errors) are not exposed.

Comment thread app/models/feedback_response_map.rb Outdated
Comment on lines +27 to +71
def self.feedback_response_report(assignment_id)
review_map_ids = ReviewResponseMap.where(reviewed_object_id: assignment_id).pluck(:id)

teams = AssignmentTeam.includes(:users).where(parent_id: assignment_id)
authors = teams.flat_map do |team|
team.users.filter_map do |user|
AssignmentParticipant.find_by(parent_id: assignment_id, user_id: user.id)
end
end

# Collect latest review responses per map (ordered newest-first)
temp_responses = Response.where(map_id: review_map_ids).order(created_at: :desc)

assignment = Assignment.find(assignment_id)
seen_map_round_keys = []

if assignment.varying_rubrics_by_round?
round_one_ids = []
round_two_ids = []
round_three_ids = []

temp_responses.each do |response|
key = "#{response.map_id}-#{response.round}"
next if seen_map_round_keys.include?(key)

seen_map_round_keys << key
round_one_ids << response.id if response.round == 1
round_two_ids << response.id if response.round == 2
round_three_ids << response.id if response.round == 3
end

[authors, round_one_ids, round_two_ids, round_three_ids]
else
all_review_response_ids = []

temp_responses.each do |response|
next if seen_map_round_keys.include?(response.map_id)

seen_map_round_keys << response.map_id
all_review_response_ids << response.id
end

[authors, all_review_response_ids]
end
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

This duplicates the new service and will drift.

Reports::FeedbackReport already owns this report, but this model method re-queries the same data, reintroduces the pre-service pluck + Ruby dedupe path, and already disagrees with the service on non-varying dedup semantics. That means callers can get different answers depending on which entry point they hit. Please keep one source of truth and adapt any legacy return shape at the boundary instead of maintaining both implementations.

🧰 Tools
🪛 RuboCop (1.86.1)

[convention] 27-71: Assignment Branch Condition size for feedback_response_report is too high. [<15, 34, 14> 39.71/20]

(Metrics/AbcSize)


[convention] 27-71: Cyclomatic complexity for feedback_response_report is too high. [11/7]

(Metrics/CyclomaticComplexity)


[convention] 27-71: Method has too many lines. [32/20]

(Metrics/MethodLength)


[convention] 27-71: Perceived complexity for feedback_response_report is too high. [12/8]

(Metrics/PerceivedComplexity)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/models/feedback_response_map.rb` around lines 27 - 71, The model method
self.feedback_response_report duplicates logic that belongs in
Reports::FeedbackReport; remove the internal query/dedupe logic in
feedback_response_report and delegate to the canonical service implementation in
Reports::FeedbackReport, adapting any legacy return shape at this boundary.
Specifically, replace the body of self.feedback_response_report(assignment_id)
with a single call to the Reports::FeedbackReport API (e.g., invoke the service
class/method that computes the authors and per-round or
all_review_response_ids), and transform the service result into the existing
return tuple the caller expects (authors + round arrays or authors +
all_review_response_ids) so callers keep the same contract while the service
remains the single source of truth. Ensure you reference the same semantics used
by the service for varying_rubrics_by_round? and deduplication (map_id vs
map_id-round) when mapping the service output back to the model method's return
shape.

Comment thread app/models/reports/base_report.rb Outdated
Comment on lines +32 to +37
def run
state = initial_state
source.find_each(batch_size: 500) do |row|
accumulate(state, grouper.call(row), row)
end
finalize(state)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the base_report.rb file
find . -name "base_report.rb" -path "*/reports/*" -type f

Repository: expertiza/reimplementation-back-end

Length of output: 117


🏁 Script executed:

# Locate feedback_report.rb
find . -name "feedback_report.rb" -path "*/reports/*" -type f

Repository: expertiza/reimplementation-back-end

Length of output: 121


🏁 Script executed:

# Search for any reports directory
find . -type d -name "reports" 2>/dev/null | head -20

Repository: expertiza/reimplementation-back-end

Length of output: 102


🏁 Script executed:

# Read base_report.rb to verify the code at lines 32-37
cat -n app/services/reports/base_report.rb | head -50

Repository: expertiza/reimplementation-back-end

Length of output: 2293


🏁 Script executed:

# Read feedback_report.rb to check source definition and dedup logic
cat -n app/services/reports/feedback_report.rb

Repository: expertiza/reimplementation-back-end

Length of output: 3123


🏁 Script executed:

# Check Rails version
cat Gemfile | grep -A 5 -B 5 "rails" | head -20

Repository: expertiza/reimplementation-back-end

Length of output: 776


🌐 Web query:

Rails find_each order preservation behavior does it respect scoped order

💡 Result:

No, Rails find_each does not respect the order from a scoped relation. When called on a relation with an existing order (e.g., Model.order(:created_at).find_each), the scoped order is ignored to enforce batching order on the primary key (or specified cursor). A warning is issued by default ("Scoped order is ignored, use :cursor with :order to configure custom order."), or an ArgumentError if config.active_record.error_on_ignored_order = true. To use custom ordering, specify cursor: :column_name, order: :desc (or :asc) explicitly in find_each options, limited to orderable columns like primary keys.

Citations:


find_each silently ignores order, breaking dedup logic.

Line 34 uses find_each(batch_size: 500), which ignores the order(created_at: :desc) scoped on the relation in FeedbackReport#source. The deduplication logic in accumulate (line 29) depends on processing newest responses first—the comments explicitly state "ordered newest-first so the first occurrence per (map, round) is the latest revision." Instead, find_each batches by primary key, causing the dedup to silently keep older responses instead of the latest ones.

In Rails 8.0, use find_each(cursor: :created_at, order: :desc) to preserve custom ordering, or refactor to push "latest-per-group" selection into SQL (e.g., using a subquery).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/services/reports/base_report.rb` around lines 32 - 37, The use of
ActiveRecord::Relation.find_each in BaseReport#run ignores the custom ordering
defined by FeedbackReport#source and breaks the dedup logic in accumulate (which
relies on "ordered newest-first"); update run to iterate while preserving
order—either switch from find_each to the Rails 8 cursor form (e.g.,
find_each(cursor: :created_at, order: :desc) or equivalent) so the relation's
order is respected, or replace the iteration by selecting latest-per-(map,
round) in SQL (push the "latest per group" logic into the source query) and then
iterate that ordered result; ensure the changes reference the run method, source
relation, and accumulate dedup behavior so newest responses are seen first.

Comment on lines +22 to +40
def grouper = ->(r) { [r.map_id, r.round] }

def initial_state
{ seen: Set.new, round_1: [], round_2: [], round_3: [], all: [] }
end

def accumulate(state, key, response)
return if state[:seen].include?(key)

state[:seen].add(key)

if @assignment.varying_rubrics_by_round?
case response.round
when 1 then state[:round_1] << response.id
when 2 then state[:round_2] << response.id
when 3 then state[:round_3] << response.id
end
else
state[:all] << response.id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Dedup is too granular when rubrics do not vary by round.

When varying_rubrics_by_round? is false, this still keys seen by [map_id, round], so one map can contribute multiple IDs to state[:all]. The legacy branch in app/models/feedback_response_map.rb dedupes by map_id only, so the new service changes the report output for multi-round assignments.

Proposed fix
-    def grouper = ->(r) { [r.map_id, r.round] }
+    def grouper
+      if `@assignment.varying_rubrics_by_round`?
+        ->(r) { [r.map_id, r.round] }
+      else
+        ->(r) { r.map_id }
+      end
+    end
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/services/reports/feedback_report.rb` around lines 22 - 40, The
deduplication key currently produced by the grouper (->(r) { [r.map_id, r.round]
}) causes multiple entries per map when `@assignment.varying_rubrics_by_round`? is
false; change the logic so the key used for state[:seen] depends on the
assignment: if varying_rubrics_by_round? then keep [r.map_id, r.round],
otherwise use r.map_id only. Update the grouper or compute key inside accumulate
(referencing grouper, accumulate, varying_rubrics_by_round?, and initial_state)
so that when rubrics do not vary by round a map_id is deduped once and
state[:all] only gets one response id per map.

asreeku and others added 7 commits April 27, 2026 13:01
FeedbackReport pipeline handles all querying, deduplication, and
round bucketing directly — the model class method is no longer called.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DeploymentPipeline now inherits BaseReport, streaming TeamsUser grouped
  by user_id instead of nested team → user loops
- Precompute taggable answer IDs and tags before the hot loop (zero DB
  queries in accumulate)
- Remove varying_rubrics_by_round? branching — item_ids filter handles
  rubric scoping at the answer level
- Extract model scopes: AnswerTag.for_deployment, TeamsUser.for_assignment,
  Answer.for_items_and_responses
- Add pipeline benefit comment to BaseReport#run

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add Item.for_questionnaire_and_type scope
- Add Response.submitted_review_responses_for scope; reuse in ReviewReport
- Fix block indentation in AnswerTaggingReport#run
- Fix Response chain indentation in fetch_responses_received_by_teams

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract ReviewResponsePipelineShared module with shared source and
  precompute_max_q_scores, included by ScoresPipeline and AvgRangesPipeline
- Filter max question scores to ReviewQuestionnaire type only
- Refactor AvgRangesPipeline to group by reviewee_id with round extracted
  in accumulate; simplify finalize with transform_values
- Improve initial_state readability with named block params and comments
- Remove unused for_course factory method
- Rename terse lambda params (r → reviewer, response_map, response)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ReviewResponseMap.for_assignment scope
- Add AssignmentQuestionnaire.for_assignment_and_type scope (generic, reusable)
- Use scopes in ReviewersPipeline source and precompute_max_q_scores

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

♻️ Duplicate comments (1)
app/models/reports/base_report.rb (1)

56-57: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

find_each may silently ignore scoped order in pipelines.

This can break correctness for reports whose source depends on explicit ordering (e.g., first-seen dedup behavior). A low-friction safeguard is to fail fast when order would be ignored.

Suggested safeguard
-      source.find_each(batch_size: 500) do |row|
+      source.find_each(batch_size: 500, error_on_ignore: true) do |row|
         accumulate(state, grouper.call(row), row)
       end

Verify whether any report source methods use explicit ordering:

#!/bin/bash
set -euo pipefail
rg -n --type=ruby 'def source|\.order\(' app/models/reports -C2
rg -n --type=ruby 'find_each\(' app/models/reports/base_report.rb -C2

Expected: if ordered sources exist, current find_each path needs explicit handling to avoid silent order loss.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: eafec4d5-6bba-4953-8e77-d50a996673ff

📥 Commits

Reviewing files that changed from the base of the PR and between 6f32448 and a118c02.

📒 Files selected for processing (11)
  • app/models/Item.rb
  • app/models/answer.rb
  • app/models/answer_tag.rb
  • app/models/assignment_questionnaire.rb
  • app/models/feedback_response_map.rb
  • app/models/reports/answer_tagging_report.rb
  • app/models/reports/base_report.rb
  • app/models/reports/review_report.rb
  • app/models/response.rb
  • app/models/review_response_map.rb
  • app/models/teams_user.rb
✅ Files skipped from review due to trivial changes (1)
  • app/models/feedback_response_map.rb
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
app/models/**/*.rb

⚙️ CodeRabbit configuration file

app/models/**/*.rb: Focus on associations, validations, callbacks, transactions, STI and polymorphic behavior,
foreign keys, data integrity, and query efficiency.

Files:

  • app/models/assignment_questionnaire.rb
  • app/models/answer.rb
  • app/models/teams_user.rb
  • app/models/Item.rb
  • app/models/answer_tag.rb
  • app/models/review_response_map.rb
  • app/models/response.rb
  • app/models/reports/base_report.rb
  • app/models/reports/review_report.rb
  • app/models/reports/answer_tagging_report.rb
🪛 ast-grep (0.42.3)
app/models/reports/answer_tagging_report.rb

[warning] 130-130: Found the use of an hardcoded passphrase for RSA. The passphrase can be easily discovered, and therefore should not be stored in source-code. It is recommended to remove the passphrase from source-code, and use system environment variables or a restricted configuration file.
Context: scope.where('LENGTH(comments) > ?', @deployment.answer_length_threshold)
Note: [CWE-798]: Use of Hard-coded Credentials [OWASP A07:2021]: Identification and Authentication Failures [REFERENCES]
https://cwe.mitre.org/data/definitions/522.html

(hardcoded-secret-rsa-passphrase-ruby)

🪛 RuboCop (1.86.2)
app/models/assignment_questionnaire.rb

[convention] 7-7: Use the lambda method for multiline lambdas.

(Style/Lambda)


[convention] 8-8: Line is too long. [121/120]

(Layout/LineLength)

app/models/answer.rb

[convention] 7-7: Inconsistent indentation detected.

(Layout/IndentationConsistency)

app/models/Item.rb

[convention] 9-9: Line is too long. [148/120]

(Layout/LineLength)

app/models/answer_tag.rb

[convention] 10-10: Unnecessary spacing detected.

(Layout/ExtraSpacing)

app/models/response.rb

[convention] 9-9: Use the lambda method for multiline lambdas.

(Style/Lambda)


[convention] 66-66: Missing space after #.

(Layout/LeadingCommentSpace)

app/models/reports/review_report.rb

[convention] 59-59: Pass &:reviewer_id as an argument to lambda instead of a block.

(Style/SymbolProc)


[convention] 109-121: Assignment Branch Condition size for accumulate is too high. [<10, 27, 8> 29.88/20]

(Metrics/AbcSize)


[convention] 109-121: Cyclomatic complexity for accumulate is too high. [9/7]

(Metrics/CyclomaticComplexity)


[convention] 109-121: Perceived complexity for accumulate is too high. [9/8]

(Metrics/PerceivedComplexity)


[convention] 148-159: Assignment Branch Condition size for accumulate is too high. [<8, 24, 8> 26.53/20]

(Metrics/AbcSize)


[convention] 148-159: Cyclomatic complexity for accumulate is too high. [9/7]

(Metrics/CyclomaticComplexity)


[convention] 148-159: Perceived complexity for accumulate is too high. [9/8]

(Metrics/PerceivedComplexity)

app/models/reports/answer_tagging_report.rb

[convention] 74-74: Pass &:user_id as an argument to lambda instead of a block.

(Style/SymbolProc)


[convention] 79-106: Assignment Branch Condition size for accumulate is too high. [<11, 24, 6> 27.07/20]

(Metrics/AbcSize)


[convention] 131-131: Modifier form of if makes the line too long.

(Style/IfUnlessModifier)


[convention] 131-131: Line is too long. [129/120]

(Layout/LineLength)


[convention] 145-145: Align .submitted_review_responses_for with Response on line 144.

(Layout/MultilineMethodCallIndentation)


[convention] 146-146: Align .pluck with Response on line 144.

(Layout/MultilineMethodCallIndentation)


[convention] 165-182: Assignment Branch Condition size for run is too high. [<13, 23, 5> 26.89/20]

(Metrics/AbcSize)


[convention] 175-175: Line is too long. [138/120]

(Layout/LineLength)


[convention] 177-177: Line is too long. [126/120]

(Layout/LineLength)

🔇 Additional comments (12)
app/models/answer_tag.rb (1)

3-14: LGTM!

app/models/Item.rb (1)

9-9: LGTM!

app/models/answer.rb (1)

7-7: LGTM!

app/models/assignment_questionnaire.rb (1)

7-9: LGTM!

app/models/response.rb (1)

9-14: LGTM!

Also applies to: 36-36, 66-66

app/models/teams_user.rb (1)

7-7: LGTM!

app/models/reports/review_report.rb (3)

13-28: LGTM!


30-47: LGTM!


88-122: LGTM!

app/models/reports/answer_tagging_report.rb (3)

23-54: LGTM!


61-72: LGTM!


108-154: LGTM!

Comment thread app/models/reports/answer_tagging_report.rb Outdated
Comment thread app/models/reports/answer_tagging_report.rb
Comment thread app/models/reports/review_report.rb Outdated
Comment thread app/models/reports/review_report.rb Outdated
Comment on lines +30 to +31
reviewers = AssignmentParticipant.where(id: distinct_reviewer_ids, parent_id: assignment_id)
reviewers.sort_by { |r| r.user&.full_name.to_s.downcase }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n --type=ruby 'class AssignmentParticipant|belongs_to :user' app/models -C2
rg -n --type=ruby 'def self.review_response_report|AssignmentParticipant\.where|sort_by \{ \|r\| r\.user' app/models/review_response_map.rb -C2

Repository: expertiza/reimplementation-back-end

Length of output: 3576


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact method and surrounding code
sed -n '1,120p' app/models/review_response_map.rb

# Confirm the association definition on AssignmentParticipant
sed -n '1,120p' app/models/assignment_participant.rb

Repository: expertiza/reimplementation-back-end

Length of output: 2822


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,120p' app/models/participant.rb
sed -n '1,200p' app/models/assignment_participant.rb

Repository: expertiza/reimplementation-back-end

Length of output: 2712


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n --type=ruby 'review_response_report\(' app -S
rg -n --type=ruby 'includes\(:user\)|preload\(:user\)' app/models app/controllers app/services -S

Repository: expertiza/reimplementation-back-end

Length of output: 370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Call sites for review_response_report
rg -n --type:ruby 'review_response_report\(' app -S

# 2) Inspect ReviewAggregator for eager loading/default_scope
rg -n --type=ruby 'module ReviewAggregator|default_scope|includes\(:user\)|preload\(:user\)' app/models -S
fd ReviewAggregator app/models -tf || true

Repository: expertiza/reimplementation-back-end

Length of output: 515


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Call sites
rg -n -S 'review_response_report\(' app

# Find ReviewAggregator definition and inspect it for eager-loading/default_scope
fd -t f 'review_aggregator*|ReviewAggregator*' app/models app -a || true
rg -n -S 'module\s+ReviewAggregator|class\s+ReviewAggregator' app/models app || true
rg -n -S 'default_scope|includes\(:user\)|preload\(:user\)' app/models/app -S || true

Repository: expertiza/reimplementation-back-end

Length of output: 494


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,200p' app/models/concerns/review_aggregator.rb
rg -n -S 'includes\(:user\)|preload\(:user\)|eager_load|joins\(:user\)' app/models/concerns/review_aggregator.rb app/models -S

Repository: expertiza/reimplementation-back-end

Length of output: 732


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -S 'review_response_report\(' . || true
rg -n -S 'review_response_report' . || true

Repository: expertiza/reimplementation-back-end

Length of output: 255


Preload user before sorting to avoid N+1 queries.

File: app/models/review_response_map.rb (Lines: 30-31)

review_response_report loads AssignmentParticipant records without eager loading :user, then sorts via reviewers.sort_by { |r| r.user... }, which will fetch user per row (since AssignmentParticipant has belongs_to :user).

Suggested fix
-    reviewers = AssignmentParticipant.where(id: distinct_reviewer_ids, parent_id: assignment_id)
+    reviewers = AssignmentParticipant.includes(:user).where(id: distinct_reviewer_ids, parent_id: assignment_id)
     reviewers.sort_by { |r| r.user&.full_name.to_s.downcase }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
reviewers = AssignmentParticipant.where(id: distinct_reviewer_ids, parent_id: assignment_id)
reviewers.sort_by { |r| r.user&.full_name.to_s.downcase }
reviewers = AssignmentParticipant.includes(:user).where(id: distinct_reviewer_ids, parent_id: assignment_id)
reviewers.sort_by { |r| r.user&.full_name.to_s.downcase }

aanand-1706 and others added 13 commits May 24, 2026 13:01
Replace single DeploymentPipeline with TaggableAnswersPipeline and
TaggingStatsPipeline. TaggableAnswersPipeline streams taggable Answer
rows accumulating answer_ids and response_ids per user via a new
Answer.taggable_for_assignment scope. TaggingStatsPipeline streams
AnswerTag rows with response context; finalize filters by response_id
membership instead of preloading all tags into memory. UserSummaryPipeline
now keys by user_id to avoid collisions for users with identical names.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sponse_id

TaggableAnswersPipeline state simplified from {answer_ids, response_ids}
to a plain array of answer_ids. Finalize converts to a Set and filters
AnswerTags directly by answer_id membership, removing the response_id
indirection and the unused response_ids Set from the pipeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fix NameError bug (return unless r → return unless reviewer) in
ReviewersPipeline. Remove stale grouper comment. Move max_q_scores
precomputation from each pipeline's initialize into ReviewReport#run
so it runs once and is passed to both ScoresPipeline and AvgRangesPipeline,
eliminating the duplicate DB query. ReviewResponsePipelineShared now
only holds the shared source definition.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add explanation of grouper's separation of concerns, how BaseReport#run
wires grouper key into accumulate, and concrete examples from
ScoresPipeline, AvgRangesPipeline, and TaggableAnswersPipeline.
Also document that all domain math belongs in accumulate, not the base.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…atest submission

Rename grouper → state_key_for across all report aggregators and BaseReport
to better express its role: given a row, return the key used to look it up
in the accumulated state.

Update submitted_review_responses_for scope to return only the latest
submitted response per (response_map, round) via a MAX(id) subquery,
ensuring that only the most recent submission per reviewer-reviewee pair
per round is counted in ScoresAggregator and AvgRangesAggregator.

Rename Pipeline → Aggregator in all comments and class references across
answer_tagging_report.rb, review_report.rb, and base_report.rb.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Store all reviewees for each reviewer instead of keeping only one entry
per reviewer. Eagerly load reviewee: :user alongside reviewer: :user to
avoid N+1 queries. Rename participant → reviewer for clarity.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…accumulate

Add caveat to accumulate description noting that the key is passed but not
enforced — subclasses are trusted to use it as the state bucket key.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BaseReport#run now accepts an optional shared_state parameter so multiple
aggregators can write into the same hash without a merge loop. When shared_state
is provided, initial_state is ignored and finalize is skipped.

TeammateReviewReport refactored into two aggregators sharing state keyed by
user_id: TeammateReviewAggregator fills reviewer info, reviewees, and
reviewed_count; TeamSizeAggregator fills teammate_count. Full state shape
initialized upfront in Hash.new block for consistency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Merge ReviewersReducer and ScoresReducer into a shared state keyed by
reviewer_id so reviewer info and scores are co-located without a merge
loop. ScoresReducer now delegates to calculate_total_score/maximum_score
(ScorableHelper) with scores and questionnaires eagerly loaded to avoid
N+1. AvgRangesReducer simplified to use aggregate_review_grade with full
eager loading. Rename Aggregator → Reducer across all report files.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove redundant "reducer" from BaseReport class description
- Fix AvgRangesReducer example: groups by team_id, not reviewee_id
- Clarify finalize-always-called sentence in BaseReport#run
- Fix teammate_review_report state key comments: reviewer_id → user_id
- Rename leftover Aggregator → Reducer in teammate_review_report comments and class names

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ntroller

- Register AnswerTaggingReport under the 'answer_tagging' type key
- Switch from report_class.new(assignment) to report_class.for_assignment(assignment)
  to match the factory method convention used by all report classes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove GET route; keep only POST /reports/fetch_response_report
- Rename action from response_report to fetch_response_report
- Drop params[:id] fallback and params.dig(:report, :type) nesting —
  JSON API clients send flat { assignment_id:, type: } body

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🚨 RSpec Tests Report

All tests passed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

♻️ Duplicate comments (1)
app/controllers/reports_controller.rb (1)

36-37: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Stop exposing internal exception messages in API responses.

Line 37 returns e.message verbatim, which can leak internals. Log server-side and return a generic 500 payload.

Proposed fix
   rescue ActiveRecord::RecordNotFound
     render json: { error: 'Assignment not found' }, status: :not_found
   rescue StandardError => e
-    render json: { error: e.message }, status: :internal_server_error
+    Rails.logger.error("[ReportsController#fetch_response_report] #{e.class}: #{e.message}")
+    render json: { error: 'Internal server error' }, status: :internal_server_error
   end

As per coding guidelines, app/controllers/**/*.rb: "Review authorization, strong parameters, HTTP status codes, and response shape consistency."

🧹 Nitpick comments (1)
app/models/reports/answer_tagging_report.rb (1)

32-41: ⚡ Quick win

Avoid reloading team memberships for every deployment.

TeamsUser.for_assignment(...).includes(:user).group_by(&:team_id) is recomputed in each DeploymentReducer instance even though assignment is constant across this run. Precompute once in AnswerTaggingReport#run and pass it down.

Proposed refactor
     def run
       per_deployment = {}
+      users_by_team = TeamsUser
+                      .for_assignment(`@reportable.id`)
+                      .includes(:user)
+                      .group_by(&:team_id)
       # The coordinator runs one DeploymentReducer per TagPromptDeployment,
       # then passes the results to UserSummaryReducer for cross-deployment totals.
       TagPromptDeployment
         .where(assignment_id: `@reportable.id`)
         .includes(:tag_prompt, :questionnaire)
         .each do |deployment|
-          per_deployment[deployment.id] = DeploymentReducer.new(`@reportable`, deployment).run
+          per_deployment[deployment.id] =
+            DeploymentReducer.new(`@reportable`, deployment, users_by_team: users_by_team).run
         end
-      def initialize(reportable, deployment)
+      def initialize(reportable, deployment, users_by_team:)
         `@reportable`    = reportable
         `@deployment`    = deployment
         `@item_ids`      = Item.for_questionnaire_and_type(deployment.questionnaire_id, deployment.question_type).pluck(:id)
-        `@users_by_team` = TeamsUser.for_assignment(deployment.assignment_id).includes(:user).group_by(&:team_id)
+        `@users_by_team` = users_by_team
       end

As per coding guidelines, "app/models/**/*.rb: Focus on ... data integrity, and query efficiency."

Also applies to: 69-74


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 14cd511c-1e7e-4427-9da1-5bcce3aeb3bd

📥 Commits

Reviewing files that changed from the base of the PR and between a118c02 and afb74c5.

📒 Files selected for processing (11)
  • app/controllers/reports_controller.rb
  • app/models/answer.rb
  • app/models/reports/answer_tagging_report.rb
  • app/models/reports/base_report.rb
  • app/models/reports/bookmark_rating_report.rb
  • app/models/reports/feedback_report.rb
  • app/models/reports/review_report.rb
  • app/models/reports/teammate_review_report.rb
  • app/models/response.rb
  • app/models/review_response_map.rb
  • config/routes.rb
📜 Review details
⏰ Context from checks skipped due to timeout of 300000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: test
  • GitHub Check: test
🧰 Additional context used
📓 Path-based instructions (3)
app/models/**/*.rb

⚙️ CodeRabbit configuration file

app/models/**/*.rb: Focus on associations, validations, callbacks, transactions, STI and polymorphic behavior,
foreign keys, data integrity, and query efficiency.

Files:

  • app/models/response.rb
  • app/models/reports/feedback_report.rb
  • app/models/reports/base_report.rb
  • app/models/reports/teammate_review_report.rb
  • app/models/reports/bookmark_rating_report.rb
  • app/models/answer.rb
  • app/models/review_response_map.rb
  • app/models/reports/review_report.rb
  • app/models/reports/answer_tagging_report.rb
app/controllers/**/*.rb

⚙️ CodeRabbit configuration file

app/controllers/**/*.rb: Review authorization, strong parameters, HTTP status codes, and response shape consistency.
Flag N+1 queries, missing request-spec coverage, and behavior changes not reflected in Swagger docs.

Files:

  • app/controllers/reports_controller.rb
config/routes.rb

⚙️ CodeRabbit configuration file

config/routes.rb: Flag duplicate or shadowed routes, surprising non-RESTful patterns, and route changes without matching request specs.

Files:

  • config/routes.rb
🧬 Code graph analysis (6)
app/models/response.rb (1)
app/models/review_response_map.rb (1)
  • reviewer_assignment (9-11)
app/models/reports/feedback_report.rb (1)
app/models/reports/review_report.rb (5)
  • source (51-55)
  • state_key_for (68-68)
  • initial_state (70-70)
  • accumulate (72-83)
  • accumulate (98-106)
app/models/reports/teammate_review_report.rb (1)
app/models/reports/review_report.rb (3)
  • run (26-45)
  • ReviewReport (17-132)
  • accumulate (72-83)
app/models/reports/bookmark_rating_report.rb (2)
app/models/reports/review_report.rb (4)
  • source (51-55)
  • state_key_for (68-68)
  • initial_state (70-70)
  • accumulate (72-83)
app/models/reports/feedback_report.rb (3)
  • initial_state (24-26)
  • accumulate (28-42)
  • finalize (44-62)
app/models/review_response_map.rb (1)
app/models/response.rb (2)
  • reviewer_assignment (30-30)
  • questionnaire (29-31)
app/models/reports/review_report.rb (1)
app/models/response.rb (1)
  • maximum_score (112-120)
🪛 RuboCop (1.86.2)
app/models/response.rb

[convention] 13-13: Align .where with joins(:response_map) on line 12.

(Layout/MultilineMethodCallIndentation)


[convention] 17-17: Align .group with joins(:response_map) on line 12.

(Layout/MultilineMethodCallIndentation)


[convention] 18-18: Align .select with joins(:response_map) on line 12.

(Layout/MultilineMethodCallIndentation)


[convention] 30-30: Redundant self detected.

(Style/RedundantSelf)

app/models/reports/feedback_report.rb

[convention] 79-79: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 80-80: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 81-81: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)

app/models/reports/base_report.rb

[convention] 87-87: Unnecessary spacing detected.

(Layout/ExtraSpacing)


[convention] 88-88: Unnecessary spacing detected.

(Layout/ExtraSpacing)

app/controllers/reports_controller.rb

[convention] 9-9: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)

app/models/reports/teammate_review_report.rb

[convention] 37-37: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 38-38: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 39-39: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 40-40: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 43-43: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 66-86: Assignment Branch Condition size for accumulate is too high. [<7, 27, 7> 28.76/20]

(Metrics/AbcSize)


[convention] 66-86: Cyclomatic complexity for accumulate is too high. [8/7]

(Metrics/CyclomaticComplexity)


[convention] 82-82: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 83-83: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 99-99: Pass &:user_id as an argument to lambda instead of a block.

(Style/SymbolProc)

config/routes.rb

[convention] 3-223: Block has too many lines. [195/120]

(Metrics/BlockLength)


[convention] 4-5: Extra empty line detected at block body beginning.

(Layout/EmptyLinesAroundBlockBody)


[convention] 12-12: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 13-18: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 19-25: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 26-41: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 28-28: Space missing after comma.

(Layout/SpaceAfterComma)


[convention] 29-29: Space missing after comma.

(Layout/SpaceAfterComma)


[convention] 30-30: Space missing after comma.

(Layout/SpaceAfterComma)


[convention] 31-31: Space missing after comma.

(Layout/SpaceAfterComma)


[convention] 33-33: Space missing after comma.

(Layout/SpaceAfterComma)


[convention] 34-34: Space missing after comma.

(Layout/SpaceAfterComma)


[convention] 39-39: Space missing after comma.

(Layout/SpaceAfterComma)


[convention] 43-48: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 43-43: Use %i or %I for an array of symbols.

(Style/SymbolArray)


[convention] 49-54: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 56-63: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 65-70: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 72-78: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 75-75: Space missing after colon.

(Layout/SpaceAfterColon)


[convention] 76-76: Space missing after colon.

(Layout/SpaceAfterColon)


[convention] 79-79: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 80-80: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 81-98: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 82-82: Use 2 (not 4) spaces for indentation.

(Layout/IndentationWidth)


[convention] 98-98: end at 98, 6 is not aligned with resources :review_mappings, only: [] do at 81, 5.

(Layout/BlockAlignment)


[convention] 100-110: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 105-109: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 109-109: end at 109, 8 is not aligned with member do at 105, 9.

(Layout/BlockAlignment)


[convention] 111-111: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 112-121: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 123-133: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 135-140: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 142-148: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 150-155: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 157-166: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 168-175: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 170-170: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 173-173: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 177-187: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 188-197: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 197-197: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 198-202: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 204-214: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 205-205: Trailing whitespace detected.

(Layout/TrailingWhitespace)


[convention] 215-219: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 220-222: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 221-221: Use %i or %I for an array of symbols.

(Style/SymbolArray)


[convention] 223-223: Final newline missing.

(Layout/TrailingEmptyLines)

app/models/reports/bookmark_rating_report.rb

[convention] 16-16: Unnecessary spacing detected.

(Layout/ExtraSpacing)


[convention] 16-16: Pass &:reviewee_id as an argument to lambda instead of a block.

(Style/SymbolProc)

app/models/answer.rb

[convention] 9-18: Inconsistent indentation detected.

(Layout/IndentationConsistency)


[convention] 9-9: Use the lambda method for multiline lambdas.

(Style/Lambda)


[convention] 11-11: Align .where with joins(response: :response_map) on line 10.

(Layout/MultilineMethodCallIndentation)


[convention] 12-12: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 13-13: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)

app/models/review_response_map.rb

[convention] 10-10: Redundant return detected.

(Style/RedundantReturn)

app/models/reports/review_report.rb

[convention] 29-29: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 30-30: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 31-31: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 33-33: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 34-34: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 42-42: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 68-68: Pass &:reviewer_id as an argument to lambda instead of a block.

(Style/SymbolProc)


[convention] 72-83: Assignment Branch Condition size for accumulate is too high. [<6, 19, 4> 20.32/20]

(Metrics/AbcSize)


[convention] 124-124: Pass &:id as an argument to lambda instead of a block.

(Style/SymbolProc)

app/models/reports/answer_tagging_report.rb

[convention] 45-45: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 72-72: Line is too long. [122/120]

(Layout/LineLength)


[convention] 84-124: Assignment Branch Condition size for finalize is too high. [<18, 38, 6> 42.47/20]

(Metrics/AbcSize)


[convention] 84-124: Method has too many lines. [29/20]

(Metrics/MethodLength)


[convention] 98-98: Line is too long. [122/120]

(Layout/LineLength)


[convention] 106-106: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 107-107: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 108-108: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 109-109: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 110-110: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 111-111: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 112-112: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 118-118: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 119-119: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 120-120: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 122-122: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 150-150: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 156-156: Pass &:team_id as an argument to lambda instead of a block.

(Style/SymbolProc)


[convention] 189-189: Line is too long. [126/120]

(Layout/LineLength)


[convention] 192-192: Pass &:user_id as an argument to lambda instead of a block.

(Style/SymbolProc)

🔇 Additional comments (13)
app/models/answer.rb (1)

8-18: LGTM!

app/models/review_response_map.rb (1)

3-3: LGTM!

Also applies to: 8-16

app/models/response.rb (1)

9-21: LGTM!

Also applies to: 23-27, 30-31, 33-43, 45-85, 88-89, 121-121

app/models/reports/base_report.rb (1)

4-5: LGTM!

Also applies to: 15-16, 20-40, 58-59, 63-83, 88-88

app/models/reports/bookmark_rating_report.rb (1)

12-26: LGTM!

app/models/reports/teammate_review_report.rb (1)

34-87: LGTM!

app/models/reports/review_report.rb (4)

26-45: LGTM!


47-56: LGTM!


72-83: LGTM!


91-107: LGTM!

app/models/reports/feedback_report.rb (3)

15-20: LGTM!


22-26: LGTM!


44-62: LGTM!

Comment on lines +21 to +23
assignment_id = params[:assignment_id]
type = params[:type] || 'basic'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="app/controllers/reports_controller.rb"

echo "== File existence =="
ls -l "$FILE" || true

echo
echo "== Show lines 1-120 with numbers =="
nl -ba "$FILE" | sed -n '1,120p'

echo
echo "== Search for fetch_response_report and rescue blocks =="
rg -n "def fetch_response_report|rescue|status:|render json:" "$FILE" || true

echo
echo "== Show full method(s) that contain assignment_id/type =="
rg -n "assignment_id|params\[:assignment_id\]|params\[:type\]" "$FILE" || true

Repository: expertiza/reimplementation-back-end

Length of output: 286


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="app/controllers/reports_controller.rb"

echo "== Show lines 1-140 =="
cat -n "$FILE" | sed -n '1,140p'

echo
echo "== Search for fetch_response_report definition and params usage =="
rg -n "def fetch_response_report|assignment_id|params\[:assignment_id\]|params\[:type\]" "$FILE" || true

echo
echo "== Search for rescue blocks / status codes / render json =="
rg -n "rescue|status:|render json:" "$FILE" || true

echo
echo "== Print method containing assignment_id/type usage (best-effort) =="
# Grab from the first occurrence of assignment_id to the end of file (small file expected)
start_line=$(rg -n "assignment_id|params\[:assignment_id\]" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${start_line:-}" ]; then
  end_line=$((start_line+80))
  cat -n "$FILE" | sed -n "${start_line},${end_line}p"
fi

Repository: expertiza/reimplementation-back-end

Length of output: 3494


Enforce strong params for assignment_id/type and return 400 for missing required input

fetch_response_report reads params[:assignment_id] / params[:type] directly (lines 21-22) and then calls Assignment.find(assignment_id); missing/invalid assignment_id is handled by the ActiveRecord::RecordNotFound rescue (lines 34-35) and returns :not_found (404) instead of a client error (400). Add permit/require for assignment_id and rescue ActionController::ParameterMissing to return :bad_request.

Proposed fix
   def fetch_response_report
-    assignment_id = params[:assignment_id]
-    type = params[:type] || 'basic'
+    request_params = params.permit(:assignment_id, :type)
+    assignment_id = request_params.require(:assignment_id)
+    type = request_params[:type].presence || 'basic'
@@
+  rescue ActionController::ParameterMissing => e
+    render json: { error: e.message }, status: :bad_request
   rescue ActiveRecord::RecordNotFound
     render json: { error: 'Assignment not found' }, status: :not_found

Comment on lines +98 to +111
matching_tags = tagging_stats.fetch(user_id, []).select { |tag| taggable_answer_ids.include?(tag[:answer_id]) }
cnt_tagged = matching_tags.size
cnt_not_tagged = cnt_taggable - cnt_tagged

tag_times_in_order = matching_tags.map { |tag| tag[:updated_at] }.sort
intervals_between_tags = tag_times_in_order.each_cons(2).map { |earlier, later| later - earlier }

{
user_id: user_id,
name: info[:name],
full_name: info[:full_name],
percentage: cnt_taggable.zero? ? '0.0' : format('%.1f', cnt_tagged.to_f / cnt_taggable * 100),
cnt_tagged: cnt_tagged,
cnt_not_tagged: cnt_not_tagged,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Count unique tagged answers, not raw tag rows.

Line 99 currently counts tag records, so repeated tags on the same answer can inflate cnt_tagged and distort cnt_not_tagged/percentage. Count unique answer_ids for progress metrics.

Proposed fix
-          matching_tags  = tagging_stats.fetch(user_id, []).select { |tag| taggable_answer_ids.include?(tag[:answer_id]) }
-          cnt_tagged     = matching_tags.size
-          cnt_not_tagged = cnt_taggable - cnt_tagged
+          matching_tags = tagging_stats.fetch(user_id, []).select do |tag|
+            taggable_answer_ids.include?(tag[:answer_id])
+          end
+          cnt_tagged_answers = matching_tags.map { |tag| tag[:answer_id] }.uniq
+          cnt_tagged         = cnt_tagged_answers.size
+          cnt_not_tagged     = cnt_taggable - cnt_tagged

As per coding guidelines, "app/models/**/*.rb: Focus on ... data integrity, and query efficiency."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
matching_tags = tagging_stats.fetch(user_id, []).select { |tag| taggable_answer_ids.include?(tag[:answer_id]) }
cnt_tagged = matching_tags.size
cnt_not_tagged = cnt_taggable - cnt_tagged
tag_times_in_order = matching_tags.map { |tag| tag[:updated_at] }.sort
intervals_between_tags = tag_times_in_order.each_cons(2).map { |earlier, later| later - earlier }
{
user_id: user_id,
name: info[:name],
full_name: info[:full_name],
percentage: cnt_taggable.zero? ? '0.0' : format('%.1f', cnt_tagged.to_f / cnt_taggable * 100),
cnt_tagged: cnt_tagged,
cnt_not_tagged: cnt_not_tagged,
matching_tags = tagging_stats.fetch(user_id, []).select do |tag|
taggable_answer_ids.include?(tag[:answer_id])
end
cnt_tagged_answers = matching_tags.map { |tag| tag[:answer_id] }.uniq
cnt_tagged = cnt_tagged_answers.size
cnt_not_tagged = cnt_taggable - cnt_tagged
tag_times_in_order = matching_tags.map { |tag| tag[:updated_at] }.sort
intervals_between_tags = tag_times_in_order.each_cons(2).map { |earlier, later| later - earlier }
{
user_id: user_id,
name: info[:name],
full_name: info[:full_name],
percentage: cnt_taggable.zero? ? '0.0' : format('%.1f', cnt_tagged.to_f / cnt_taggable * 100),
cnt_tagged: cnt_tagged,
cnt_not_tagged: cnt_not_tagged,
🧰 Tools
🪛 RuboCop (1.86.2)

[convention] 98-98: Line is too long. [122/120]

(Layout/LineLength)


[convention] 106-106: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 107-107: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 108-108: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 109-109: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 110-110: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 111-111: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)

Comment thread app/models/reports/answer_tagging_report.rb
Comment on lines +33 to +41
if @reportable.varying_rubrics_by_round?
case response.round
when 1 then state[:round_1] << response.id
when 2 then state[:round_2] << response.id
when 3 then state[:round_3] << response.id
end
else
state[:all] << response.id
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Responses with nil or unexpected round values are silently dropped.

When varying_rubrics_by_round? is true, responses with round values other than 1, 2, or 3 (including nil) are added to the seen set but never accumulated into any bucket. This could silently exclude valid data.

Consider adding a fallback or logging for unexpected values.

🛡️ Proposed fix with fallback bucket or default
       if `@reportable.varying_rubrics_by_round`?
         case response.round
         when 1 then state[:round_1] << response.id
         when 2 then state[:round_2] << response.id
         when 3 then state[:round_3] << response.id
+        else
+          Rails.logger.warn("FeedbackReport: unexpected round=#{response.round.inspect} for response #{response.id}")
         end
       else
         state[:all] << response.id
       end

Comment on lines +66 to +84
def fetch_authors
teams = AssignmentTeam.includes(:users).where(parent_id: @reportable.id)
teams.flat_map do |team|
team.users.filter_map do |user|
AssignmentParticipant.find_by(parent_id: @reportable.id, user_id: user.id)
end
end
end

def format_participant(p)
return {} unless p

{
id: p.id,
user_id: p.user_id,
name: p.user&.name,
full_name: p.user&.full_name
}
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

N+1 query pattern: individual find_by calls inside nested loop.

fetch_authors issues one AssignmentParticipant.find_by query per user per team. For an assignment with many teams/users, this creates O(n) queries. Additionally, the returned participants don't preload :user, causing another N+1 when format_participant accesses p.user&.name.

Batch-load participants with a single query and eager-load users.

🐛 Proposed fix
     def fetch_authors
-      teams = AssignmentTeam.includes(:users).where(parent_id: `@reportable.id`)
-      teams.flat_map do |team|
-        team.users.filter_map do |user|
-          AssignmentParticipant.find_by(parent_id: `@reportable.id`, user_id: user.id)
-        end
-      end
+      user_ids = AssignmentTeam
+                   .joins(:teams_users)
+                   .where(parent_id: `@reportable.id`)
+                   .pluck('teams_users.user_id')
+                   .uniq
+
+      AssignmentParticipant
+        .includes(:user)
+        .where(parent_id: `@reportable.id`, user_id: user_ids)
     end
🧰 Tools
🪛 RuboCop (1.86.2)

[convention] 79-79: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 80-80: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)


[convention] 81-81: Align the keys of a hash literal if they span more than one line.

(Layout/HashAlignment)

Comment on lines +117 to +122
class AvgRangesReducer < BaseReport
def source
AssignmentTeam
.where(parent_id: @reportable.id)
.includes(review_mappings: { responses: { scores: :item } })
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for aggregate_review_grade implementation and trace its calls
ast-grep --pattern 'def aggregate_review_grade
  $$$
end'

# Also check for maximum_score calls in related code
rg -n 'maximum_score' --type ruby -C 3

Repository: expertiza/reimplementation-back-end

Length of output: 10973


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Show AvgRangesReducer and nearby reducers in review_report.rb
sed -n '80,170p' app/models/reports/review_report.rb | cat -n

# 2) Inspect AssignmentTeam#compute_average_review_score implementation
rg -n "def compute_average_review_score" app/models/assignment_team.rb
python3 - <<'PY'
import re, itertools, pathlib
p=pathlib.Path("app/models/assignment_team.rb")
text=p.read_text().splitlines()
for i,line in enumerate(text, start=1):
    if "def compute_average_review_score" in line:
        start=max(1,i-10); end=min(len(text), i+160)
        for j in range(start, end+1):
            print(f"{j:6d}:{text[j-1]}")
        break
PY

# 3) Inspect Response#maximum_score body (to see how questionnaire is accessed)
python3 - <<'PY'
import pathlib
p=pathlib.Path("app/models/response.rb")
lines=p.read_text().splitlines()
for i,line in enumerate(lines, start=1):
    if re.search(r"def maximum_score\b", line):
        start=max(1,i-10); end=min(len(lines), i+120)
        for j in range(start,end+1):
            print(f"{j:6d}:{lines[j-1]}")
        break
PY

# 4) Inspect ScorableHelper maximum_score implementation (if used by reducer/report)
sed -n '1,140p' app/helpers/scorable_helper.rb | cat -n

# 5) Find the code path that does "questionnaire.max_question_score" specifically
rg -n "max_question_score" app/models app/helpers | head -n 50

Repository: expertiza/reimplementation-back-end

Length of output: 2530


🏁 Script executed:

#!/bin/bash
set +euo pipefail
# 1) Find compute_average_review_score and show around it
rg -n "def compute_average_review_score" app/models/assignment_team.rb
python3 - <<'PY'
import pathlib, re
p=pathlib.Path("app/models/assignment_team.rb")
lines=p.read_text().splitlines()
for i,l in enumerate(lines, start=1):
    if "def compute_average_review_score" in l:
        start=max(1,i-20); end=min(len(lines), i+220)
        for j in range(start,end+1):
            print(f"{j:6d}:{lines[j-1]}")
        break
PY

# 2) Show aggregate_review_grade implementation around first occurrence
rg -n "def aggregate_review_grade" app/models/assignment_team.rb
python3 - <<'PY'
import pathlib
p=pathlib.Path("app/models/assignment_team.rb")
lines=p.read_text().splitlines()
idxs=[i for i,l in enumerate(lines, start=1) if "def aggregate_review_grade" in l]
for k,i in enumerate(idxs[:2], start=1):
    print("\n--- aggregate_review_grade block",k,"at line",i,"---")
    start=max(1,i-20); end=min(len(lines), i+220)
    for j in range(start,end+1):
        print(f"{j:6d}:{lines[j-1]}")
PY

# 3) Show Response#maximum_score implementation
python3 - <<'PY'
import pathlib, re
p=pathlib.Path("app/models/response.rb")
lines=p.read_text().splitlines()
for i,line in enumerate(lines, start=1):
    if re.search(r"def maximum_score\b", line):
        start=max(1,i-20); end=min(len(lines), i+260)
        for j in range(start,end+1):
            print(f"{j:6d}:{lines[j-1]}")
        break
PY

# 4) Show ScorableHelper maximum_score implementation
python3 - <<'PY'
import pathlib, re
p=pathlib.Path("app/helpers/scorable_helper.rb")
lines=p.read_text().splitlines()
for i,line in enumerate(lines, start=1):
    if re.search(r"def maximum_score\b", line):
        start=max(1,i-20); end=min(len(lines), i+260)
        for j in range(start,end+1):
            print(f"{j:6d}:{lines[j-1]}")
        # likely first occurrence; continue searching for next def maximum_score after that
        # but keep minimal
        break
PY

# 5) Find "questionnaire.max_question_score" usage
rg -n "questionnaire\.max_question_score" app/models app/helpers

Repository: expertiza/reimplementation-back-end

Length of output: 12594


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# AssignmentTeam: locate compute_average_review_score and show its body
rg -n "def compute_average_review_score" app/models/assignment_team.rb
python3 - <<'PY'
import pathlib,re
p=pathlib.Path("app/models/assignment_team.rb")
lines=p.read_text().splitlines()
for i,l in enumerate(lines, start=1):
    if "def compute_average_review_score" in l:
        start=max(1,i-20); end=min(len(lines), i+260)
        for j in range(start,end+1):
            print(f"{j:6d}:{lines[j-1]}")
        break
PY

# ScorableHelper: show maximum_score + questionnaire_by_answer
rg -n "def maximum_score|def questionnaire_by_answer" app/helpers/scorable_helper.rb
python3 - <<'PY'
import pathlib,re
p=pathlib.Path("app/helpers/scorable_helper.rb")
lines=p.read_text().splitlines()
# print around first def questionnaire_by_answer and surrounding maximum_score
for i,l in enumerate(lines, start=1):
    if "def maximum_score" in l:
        start=max(1,i-10); end=min(len(lines), i+110)
        for j in range(start,end+1):
            print(f"{j:6d}:{lines[j-1]}")
        break
PY

# Review report: show header comment about eager-loading
sed -n '1,120p' app/models/reports/review_report.rb | cat -n

# Response model: show maximum_score method around its definition
rg -n "def maximum_score" app/models/response.rb
python3 - <<'PY'
import pathlib,re
p=pathlib.Path("app/models/response.rb")
lines=p.read_text().splitlines()
for i,l in enumerate(lines, start=1):
    if re.search(r"def maximum_score\b", l):
        start=max(1,i-25); end=min(len(lines), i+120)
        for j in range(start,end+1):
            print(f"{j:6d}:{lines[j-1]}")
        break
PY

Repository: expertiza/reimplementation-back-end

Length of output: 61


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find where compute_average_review_score is defined
rg -n "def compute_average_review_score" app/models

# 2) Also find usages that might show how it's called / what it returns
rg -n "compute_average_review_score" app/models

# 3) Locate any module/concern that is likely included by AssignmentTeam
rg -n "ReviewAggregator|include .*Aggregator|include .*Scorable|compute_average_review_score" app/models/assignment_team.rb

# 4) Inspect the first definition location(s) of compute_average_review_score
python3 - <<'PY'
import subprocess, re, os, textwrap, pathlib, json
import sys
res=subprocess.check_output(["bash","-lc","rg -n \"def compute_average_review_score\" app/models"]).decode()
print(res)
PY

Repository: expertiza/reimplementation-back-end

Length of output: 856


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Review aggregator: how it computes average and calls response scoring
sed -n '1,220p' app/models/concerns/review_aggregator.rb | cat -n

# Show full Response#maximum_score method body (around it)
python3 - <<'PY'
import pathlib,re
p=pathlib.Path("app/models/response.rb")
lines=p.read_text().splitlines()
idx=None
for i,l in enumerate(lines, start=1):
    if re.search(r"def maximum_score\b", l):
        idx=i; break
if idx is None:
    raise SystemExit("maximum_score not found")
start=max(1,idx-20); end=min(len(lines), idx+80)
for j in range(start,end+1):
    print(f"{j:6d}:{lines[j-1]}")
PY

# Show ScorableHelper maximum_score + questionnaire_by_answer (the part that loads questionnaire)
sed -n '1,130p' app/helpers/scorable_helper.rb | cat -n

# Check whether any eager-load helper exists for item->questionnaire
rg -n "includes\\(.*questionnaire|questionnaire_by_answer\\(|Item\\.find\\(.*\\)\\.questionnaire" app/models app/helpers

Repository: expertiza/reimplementation-back-end

Length of output: 5665


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find how review mappings aggregate reviewer scores
rg -n "aggregate_reviewers_score" app/models

# Inspect that method definition
python3 - <<'PY'
import pathlib, re, itertools
root=pathlib.Path("app/models")
# find file containing aggregate_reviewers_score
paths=[]
for p in root.rglob("*.rb"):
    if "aggregate_reviewers_score" in p.read_text(errors="ignore"):
        paths.append(p)
print("FILES:", paths)
PY

# Show definition(s) with context
rg -n "def aggregate_reviewers_score" app/models -S

# Inspect Response#maximum_score body around where questionnaire is set
python3 - <<'PY'
import pathlib, re
p=pathlib.Path("app/models/response.rb")
lines=p.read_text().splitlines()
for i,l in enumerate(lines, start=1):
    if re.search(r"def maximum_score\b", l):
        start=max(1,i-30); end=min(len(lines), i+160)
        for j in range(start,end+1):
            print(f"{j:6d}:{lines[j-1]}")
        break
PY

# Inspect Response#questionnaire_by_answer call site around line ~70-110
sed -n '60,140p' app/models/response.rb | cat -n

# Inspect AssignmentTeam review_mappings association to ensure it maps to ReviewResponseMap
rg -n "has_many :review_mappings|review_mappings" app/models/assignment_team.rb | head -n 50

Repository: expertiza/reimplementation-back-end

Length of output: 5596


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect ResponseMap file for includes and relevant methods (questionnaire_by_answer / maximum_score / aggregate_reviewers_score)
sed -n '1,220p' app/models/response_map.rb | cat -n

# Inspect Response model around questionnaire/questionnaire_by_answer usage
sed -n '1,200p' app/models/response.rb | cat -n

# Check whether ResponseMap includes ScorableHelper (or defines questionnaire_by_answer itself)
rg -n "include .*ScorableHelper|def questionnaire_by_answer|def questionnaire\b" app/models/response_map.rb app/models/response.rb app/helpers/scorable_helper.rb

Repository: expertiza/reimplementation-back-end

Length of output: 13792


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Participant model for assignment association
rg -n "class Participant\b|belongs_to :assignment\b|has_many :assignment_questionnaires\b|assignment_questionnaires" app/models/participant*.rb app/models/*participant*.rb || true

# Locate where assignment_questionnaires association is defined (Assignment model)
rg -n "assignment_questionnaires" app/models | head -n 50

# Inspect AssignmentQuestionnaire model for belongs_to :questionnaire
rg -n "class AssignmentQuestionnaire\b" app/models | head -n 20
fd -e rb -p "assignment_questionnaire.rb" app/models 2>/dev/null || true
fd "assignment_questionnaire" app/models -t f | head -n 20

# Read likely model files with cat -n around association lines
for f in app/models/participant.rb app/models/participants.rb app/models/assignment.rb app/models/assignment_questionnaire.rb app/models/*assignment_questionnaire*.rb; do
  if [ -f "$f" ]; then
    echo "---- $f ----"
    rg -n "belongs_to :assignment|assignment_questionnaires|belongs_to :questionnaire" "$f" || true
  fi
done

Repository: expertiza/reimplementation-back-end

Length of output: 1621


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '35,90p' app/models/reports/review_report.rb | cat -n

Repository: expertiza/reimplementation-back-end

Length of output: 2602


Fix AvgRangesReducer eager-loading to avoid N+1 in Response#maximum_score

AvgRangesReducer eager-loads scores: :item, but Response#maximum_score uses questionnaire.max_question_score where questionnaire comes from reviewer_assignment.assignment_questionnaires.find_by(used_in_round: round).questionnaire. reviewer.assignment.assignment_questionnaires/their questionnaire aren’t eager-loaded here, so scoring can trigger per-response queries (the item: :questionnaire path doesn’t match this call chain).

🐛 Proposed fix
       def source
         AssignmentTeam
           .where(parent_id: `@reportable.id`)
-          .includes(review_mappings: { responses: { scores: :item } })
+          .includes(review_mappings: {
+            reviewer: { assignment: { assignment_questionnaires: :questionnaire } },
+            responses: { scores: :item }
+          })
       end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
class AvgRangesReducer < BaseReport
def source
AssignmentTeam
.where(parent_id: @reportable.id)
.includes(review_mappings: { responses: { scores: :item } })
end
class AvgRangesReducer < BaseReport
def source
AssignmentTeam
.where(parent_id: `@reportable.id`)
.includes(review_mappings: {
reviewer: { assignment: { assignment_questionnaires: :questionnaire } },
responses: { scores: :item }
})
end

Comment on lines +99 to +107
def state_key_for = ->(teams_user) { teams_user.user_id }

def initial_state = {}

def accumulate(state, user_id, _teams_user)
# teammate_count is the number of TeamsUser rows for this user's team,
# which equals the number of teammates (including themselves).
state[user_id][:teammate_count] += 1 if state.key?(user_id)
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Teammate count is computed per user-row, not per team size.

Line 99 keys by teams_user.user_id, and Line 106 increments once for that user’s membership row. That yields teammate_count ≈ 1 per reviewer (or duplicate membership count), not the number of teammates in the reviewer’s team.

💡 Suggested fix
 class TeamSizeReducer < BaseReport
   def source
-    TeamsUser.for_assignment(`@reportable.id`).includes(:user)
+    TeamsUser.for_assignment(`@reportable.id`).select(:user_id, :team_id)
   end

   def state_key_for = ->(teams_user) { teams_user.user_id }

   def initial_state = {}

-  def accumulate(state, user_id, _teams_user)
-    # teammate_count is the number of TeamsUser rows for this user's team,
-    # which equals the number of teammates (including themselves).
-    state[user_id][:teammate_count] += 1 if state.key?(user_id)
+  def accumulate(state, user_id, teams_user)
+    return unless state.key?(user_id)
+
+    `@team_sizes` ||= TeamsUser.for_assignment(`@reportable.id`).group(:team_id).count
+    state[user_id][:teammate_count] = `@team_sizes`[teams_user.team_id].to_i
   end
 end

As per coding guidelines, app/models/**/*.rb: “Focus on ... data integrity, and query efficiency.”

🧰 Tools
🪛 RuboCop (1.86.2)

[convention] 99-99: Pass &:user_id as an argument to lambda instead of a block.

(Style/SymbolProc)

Comment thread config/routes.rb
Comment on lines +198 to 202
resources :reports, only: [] do
collection do
post :fetch_response_report
end
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Request specs mentioning reports endpoint =="
rg -nP -C2 --iglob '**/*_spec.rb' 'fetch_response_report|/reports/fetch_response_report|ReportsController' || true

echo
echo "== Swagger/OpenAPI docs mentioning reports endpoint =="
rg -nP -C2 \
  --iglob '**/*swagger*' \
  --iglob '**/*openapi*' \
  --iglob '**/*.yml' \
  --iglob '**/*.yaml' \
  'fetch_response_report|/reports/fetch_response_report' || true

Repository: expertiza/reimplementation-back-end

Length of output: 183


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Occurrences of fetch_response_report =="
rg -n --hidden --no-ignore --glob '!**/node_modules/**' 'fetch_response_report' . || true

echo
echo "== Occurrences of POST /reports (non-RSpec context) =="
rg -n --hidden --no-ignore --glob '!**/node_modules/**' 'POST\s*/reports' . || true

echo
echo "== Spec files referencing reports endpoint/action =="
rg -n -S --glob '**/*_spec.rb' 'reports.*fetch_response_report|fetch_response_report.*reports|/reports/fetch_response_report' . || true

echo
echo "== OpenAPI/Swagger artifacts (common file types) =="
fd -t f -e rb -e ts -e js -e json -e xml . 'openapi|swagger' 2>/dev/null || true

# List likely doc files and then grep for the route/action
echo
echo "== Swagger/OpenAPI files containing reports/fetch_response_report or fetch_response_report =="
# Use git ls-files if available for consistent file set
if git ls-files >/dev/null 2>&1; then
  DOCS=$(git ls-files | rg -i 'swagger|openapi|api-doc|documentation' || true)
  printf "%s\n" "$DOCS" | rg -iE '\.ya?ml|\.json' || true | head -n 200
  echo
  printf "%s\n" "$DOCS" | rg -iE '\.ya?ml|\.json' | xargs -r rg -n -i 'fetch_response_report|/reports/fetch_response_report' || true
fi

Repository: expertiza/reimplementation-back-end

Length of output: 1491


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Inspect ReportsController for inline API docs/annotations =="
if [ -f app/controllers/reports_controller.rb ]; then
  sed -n '1,120p' app/controllers/reports_controller.rb
fi

echo
echo "== Search request specs for fetch_response_report action/routes =="
rg -n --glob '**/*_spec.rb' \
  'fetch_response_report|/reports/fetch_response_report|ReportsController' . || true

echo
echo "== Search for controller/action specs (reports_controller.rb mention) =="
rg -n --glob '**/*_spec.rb' \
  'ReportsController|reports_controller' . || true

echo
echo "== Search for OpenAPI/Swagger files (YAML/JSON) referencing the route/action =="
# Find likely spec/doc files first, then grep them
DOC_FILES=$(
  (git ls-files 2>/dev/null || true) | \
  rg -i 'openapi|swagger|api-doc|documentation' || true
)

if [ -z "${DOC_FILES}" ]; then
  echo "(No obvious swagger/openapi doc files found by filename heuristics.)"
else
  echo "$DOC_FILES" | rg -i '\.ya?ml|\.json' | head -n 200
  echo
  echo "Matches in those files:"
  echo "$DOC_FILES" | rg -i '\.ya?ml|\.json' | xargs -r rg -n -i \
    'fetch_response_report|/reports/fetch_response_report|reports.*fetch_response_report' || true
fi

echo
echo "== Search for OpenAPI/Swagger annotations in rb code =="
rg -n --glob '**/*.rb' \
  'openapi|swagger|swagger_ui|rswag|api_doc|`@swagger`|OpenAPI|Swagger' \
  . || true

Repository: expertiza/reimplementation-back-end

Length of output: 7167


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Search swagger/v1/swagger.yaml for fetch_response_report or /reports/fetch... =="
if [ -f swagger/v1/swagger.yaml ]; then
  rg -n 'fetch_response_report|/reports/fetch_response_report|fetch response report|fetchResponse' swagger/v1/swagger.yaml || true
  echo
  echo "== Search swagger/v1/swagger.yaml for any /reports/ paths =="
  rg -n '/reports/' swagger/v1/swagger.yaml || true
else
  echo "swagger/v1/swagger.yaml not found"
fi

echo
echo "== Search request specs under spec/requests/api/v1 for reports or fetch_response =="
rg -n --glob '**/*_spec.rb' \
  'fetch_response_report|fetch_response|/reports/fetch_response_report|ReportsController|reports/' \
  spec/requests/api/v1 || true

echo
echo "== Search request specs anywhere for fetch_response_report (broader) =="
rg -n --glob '**/*_spec.rb' \
  'fetch_response_report|/reports/fetch_response_report|ReportsController' . || true

Repository: expertiza/reimplementation-back-end

Length of output: 383


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== swagger/v1/swagger.yaml size =="
if [ -f swagger/v1/swagger.yaml ]; then
  wc -l swagger/v1/swagger.yaml
fi

echo
echo "== Search swagger/v1/swagger.yaml for any 'reports' path keys =="
if [ -f swagger/v1/swagger.yaml ]; then
  rg -n '/reports|reports:' swagger/v1/swagger.yaml || true
fi

echo
echo "== Search swagger/v1/swagger.yaml for fetch.*report patterns =="
if [ -f swagger/v1/swagger.yaml ]; then
  rg -n -i 'fetch[^\\n]{0,40}report|report[^\\n]{0,40}fetch|fetch_response|fetchresponse' swagger/v1/swagger.yaml || true
fi

echo
echo "== Search request specs for 'reports' route/prefixes =="
rg -n --glob '**/*_spec.rb' 'post\s*\(?[\'"]/reports|/reports' spec/requests || true

echo
echo "== Search request specs for 'reports_controller' usage =="
rg -n --glob '**/*_spec.rb' 'reports_controller|ReportsController' spec/requests || true

Repository: expertiza/reimplementation-back-end

Length of output: 419


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== routes.rb context around line 198-202 =="
nl -ba config/routes.rb | sed -n '160,240p'

echo
echo "== Search swagger/v1/swagger.yaml for fetch_response_report (case-insensitive) =="
rg -n -i 'fetch[_ -]*response[_ -]*report' swagger/v1/swagger.yaml || true

echo
echo "== Search swagger/v1/swagger.yaml for 'reports' path entries =="
rg -n '/reports(/|$)|reports:' swagger/v1/swagger.yaml || true

echo
echo "== Search request specs for fetch_response_report =="
rg -n --glob '**/*_spec.rb' 'fetch_response_report|fetch response report|/reports/fetch_response_report' spec || true

echo
echo "== Search request specs for any /reports usage =="
rg -n --glob '**/*_spec.rb' '/reports' spec/requests || true

echo
echo "== Search request specs for reports_controller / ReportsController usage =="
rg -n --glob '**/*_spec.rb' 'ReportsController|reports_controller' spec/requests || true

echo
echo "== Search request specs for 'reports' keyword (first 50 matches) =="
rg -n --glob '**/*_spec.rb' '\breports\b' spec/requests | head -n 50 || true

Repository: expertiza/reimplementation-back-end

Length of output: 184


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== routes.rb context around line 198-202 (using cat -n) =="
cat -n config/routes.rb | sed -n '175,230p'

echo
echo "== Search swagger/v1/swagger.yaml for fetch_response_report =="
rg -n -i 'fetch[_ -]*response[_ -]*report' swagger/v1/swagger.yaml || true

echo
echo "== Search swagger/v1/swagger.yaml for reports paths =="
rg -n '/reports(/|reports:' swagger/v1/swagger.yaml || true

echo
echo "== Search request specs for fetch_response_report =="
rg -n --glob '**/*_spec.rb' 'fetch_response_report|/reports/fetch_response_report' spec || true

echo
echo "== Search request specs for ReportsController =="
rg -n --glob '**/*_spec.rb' 'ReportsController|reports_controller' spec/requests || true

echo
echo "== Search request specs for any reports prefix =="
rg -n --glob '**/*_spec.rb' '/reports|post\s*\(?[\'"]/reports' spec/requests || true

Repository: expertiza/reimplementation-back-end

Length of output: 2571


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== swagger/v1/swagger.yaml: fetch_response_report matches =="
rg -n -i 'fetch_response_report' swagger/v1/swagger.yaml || true

echo
echo "== swagger/v1/swagger.yaml: /reports matches (anywhere) =="
rg -n -i '/reports' swagger/v1/swagger.yaml || true

echo
echo "== swagger/v1/swagger.yaml: reports: matches (path keys) =="
rg -n -i 'reports:' swagger/v1/swagger.yaml || true

echo
echo "== swagger/v1/swagger.yaml: reports (word) matches (first 50) =="
rg -n -i '\breports\b' swagger/v1/swagger.yaml | head -n 50 || true

echo
echo "== request specs: fetch_response_report matches =="
rg -n --glob '**/*_spec.rb' -i 'fetch_response_report|/reports/fetch_response_report|fetch response report' spec || true

echo
echo "== request specs: /reports matches under spec/requests (first 50) =="
rg -n --glob '**/*_spec.rb' -i '/reports' spec/requests | head -n 50 || true

echo
echo "== request specs: reports_controller matches =="
rg -n --glob '**/*_spec.rb' -i 'ReportsController|reports_controller' spec/requests || true

Repository: expertiza/reimplementation-back-end

Length of output: 498


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== swagger/v1/swagger.yaml: head (top 80 lines) =="
head -n 80 swagger/v1/swagger.yaml

echo
echo "== swagger/v1/swagger.yaml: locate 'paths:' =="
rg -n '^\s*paths:\s*$' swagger/v1/swagger.yaml || true

echo
echo "== swagger/v1/swagger.yaml: show around first 'paths:' occurrence =="
LINE=$(rg -n '^\s*paths:\s*$' swagger/v1/swagger.yaml | head -n 1 | cut -d: -f1 || true)
if [ -n "${LINE}" ]; then
  start=$((LINE-20))
  end=$((LINE+120))
  if [ "$start" -lt 1 ]; then start=1; fi
  sed -n "${start},${end}p" swagger/v1/swagger.yaml
else
  echo "No 'paths:' key found."
fi

echo
echo "== swagger/v1/swagger.yaml: broader substring searches =="
rg -n -i 'report' swagger/v1/swagger.yaml || true
rg -n -i 'fetch' swagger/v1/swagger.yaml || true
rg -n -i 'reports' swagger/v1/swagger.yaml || true

echo
echo "== request specs: list any files with 'report' in name =="
find spec -type f -name '*report*' | head -n 200 || true

echo
echo "== request specs: broader searches (limit output) =="
rg -n -i '\breport\b' spec/requests | head -n 50 || true
rg -n -i 'fetch' spec/requests | head -n 50 || true
rg -n -i 'reports' spec/requests | head -n 50 || true

Repository: expertiza/reimplementation-back-end

Length of output: 5830


Add/confirm request-spec + Swagger/OpenAPI coverage for POST /reports/fetch_response_report.

resources :reports, only: [] do
  collection do
    post :fetch_response_report
  end
end

config/routes.rb and app/controllers/reports_controller.rb define ReportsController#fetch_response_report, but there are no corresponding request specs or Swagger entries for this endpoint. Add an rswag request spec that documents and covers success (200) plus error paths: 422 (unknown type), 404 (assignment not found), and 500 (unexpected error).

🧰 Tools
🪛 RuboCop (1.86.2)

[convention] 198-202: Inconsistent indentation detected.

(Layout/IndentationConsistency)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🚨 RSpec Tests Report

All tests passed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

🚨 RSpec Tests Report

All tests passed.

@aanand-1706 aanand-1706 reopened this May 29, 2026
@github-actions

Copy link
Copy Markdown

🚨 RSpec Tests Report

All tests passed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant