Skip to content

Export linked grader hint feedback#780

Open
jon-bell wants to merge 4 commits into
stagingfrom
cursor-export-grader-extra-data-d4e6
Open

Export linked grader hint feedback#780
jon-bell wants to merge 4 commits into
stagingfrom
cursor-export-grader-extra-data-d4e6

Conversation

@jon-bell
Copy link
Copy Markdown
Contributor

@jon-bell jon-bell commented May 22, 2026

Summary

  • Export hint feedback by the emitted grader_result_tests ids so hints.json only contains rows linked to exported tests.
  • Preserve the existing extra_data field on emitted grader test records.
  • Export per-submission student engagement for error-pinned discussion posts into error-pin-engagement.json.

Testing

  • npm run format
  • npm run lint
  • npm test -- --runTestsByPath tests/unit/assessment-export-tokenization.test.ts tests/unit/assessment-export-selectors.test.ts
  • node -e "const fs=require('fs'); const edge=fs.readFileSync('supabase/functions/cli/commands/assessment.ts','utf8'); const cli=fs.readFileSync('cli/commands/assessment/export.ts','utf8'); if(!edge.includes('kind: \\\"error_pin_engagement\\\"')) throw new Error('edge stream does not emit error_pin_engagement'); if(!edge.includes('.from(\\\"discussion_thread_read_status\\\")')) throw new Error('read status is not exported'); if(!edge.includes('.from(\\\"discussion_thread_likes\\\")')) throw new Error('likes are not exported'); if(!cli.includes('error-pin-engagement.json')) throw new Error('CLI does not write error-pin-engagement.json'); console.log('assessment export includes error-pin engagement read/like wiring');"

Notes

  • Direct npx -y deno check cli/commands/assessment.ts was attempted; it reaches the function but fails on the generated supabase/functions/_shared/SupabaseTypes.d.ts export const Constants ambient declaration before checking this patch.
  • npx tsc --noEmit --pretty false was attempted; it fails on the pre-existing tests/e2e/active-submission-gradebook-db.spec.ts(49,5) nullable string assignment.
Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Added --skip-gradebook option to exclude gradebook data from exports.
    • Added --all-submissions option to export all submission attempts.
    • Added error-pin engagement tracking to exports.
  • Improvements

    • Enhanced transient error detection for improved export reliability.
    • Improved export documentation and deanonymization guidance.

Review Change Stack

Co-authored-by: Jonathan Bell <jon@jonbell.net>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 22, 2026

Walkthrough

This PR introduces server-side pepper-based HMAC tokenization for assessment exports, adds new CLI options for selective export (skip gradebook, include all submissions), refactors the export pipeline into sectioned streams with error-pin engagement tracking, and improves transient stream error detection.

Changes

Assessment Export with Server-Side Pepper Tokenization

Layer / File(s) Summary
Tokenization foundation with server pepper
supabase/functions/cli/utils/tokenization.ts, supabase/migrations/20260522143000_assessment_export_pepper.sql, tests/unit/assessment-export-tokenization.test.ts
New two-step HMAC tokenization: server pepper (32+ bytes base64 from vault) combined with client salt via key derivation. Database migration creates vault secret and function to read/validate pepper. Tests confirm salt + pepper both required for token consistency, and pepper changes alter tokens.
CLI options and transient error handling
cli/commands/assessment/deanonymize.ts, cli/commands/assessment/export.ts, cli/utils/transientRetry.ts, tests/unit/cli-transient-retry.test.ts
Adds --skip-gradebook and --all-submissions options with validation (incompatible options rejected). Updates documentation to explain salt + pepper tokenization semantics for hash/opaque modes. Expands transient error patterns to detect truncated streams and resource limit failures.
CLI export handler with new options
cli/commands/assessment/export.ts (handler logic)
Passes skip_gradebook and all_submissions to server, conditionally skips gradebook streaming, accumulates error-pin engagement totals, backfills manifest with export flags, refactors assignment export to stream each section with transient retry per section.
Server export setup with tokenization
supabase/functions/cli/commands/assessment.ts (preamble & setup)
Preamble accepts skip_gradebook flag, wires createExportTokenizer for identity modes, conditionally counts gradebook columns, batches user info queries to avoid large .in() filters.
Sectioned assignment export dispatcher
supabase/functions/cli/commands/assessment.ts (sections & dispatch)
Introduces AssignmentExportSection type (meta/submissions/scores/tests/engagement/all) with per-section batch pagination parameters. Establishes export context with scope loaders (submissions, reviews). Routes handler by section with transient retry.
Submission and scoring export streams
supabase/functions/cli/commands/assessment.ts (submission & score flows)
Refactors submission export to include is_active flag and optional all-attempts mode. Reorganizes score streaming to preload review scope ids. Updates grader test export to return structured result (testCount, graderTestIds) and slim large extra_data blobs.
Hint export with deduplication and error-pin engagement
supabase/functions/cli/commands/assessment.ts (hints & engagement)
Extends hint export to emit both instructor feedback and LLM-derived hints with deduplication by (grader_result_test + rater), marking feedback source. Adds error-pin engagement streaming with participant/match/discussion loaders and derived mappings. Aggregates engagement counts end-to-end.
Gradebook and roster export with new tokenizer
supabase/functions/cli/commands/assessment.ts (gradebook & roster)
Updates gradebook and roster export to use createExportTokenizer. Clarifies roster documentation: only hash mode is joinable across exports; opaque uses per-run salt and cannot be reproduced offline.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🌱 Salt and pepper dance with care,
Each hash a token, tokens square,
Sections stream, engagement grows,
All submissions now exposed,
Hints dedupe through feedback rows,
Error pins where truth now flows.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Export linked grader hint feedback' directly corresponds to the main objective of exporting hint feedback keyed by grader_result_tests ids, which is the primary change highlighted in the PR summary.
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.

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


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.

Co-authored-by: Jonathan Bell <jon@jonbell.net>
@argos-ci
Copy link
Copy Markdown

argos-ci Bot commented May 22, 2026

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
default (Inspect) ⚠️ Changes detected (Review) 25 changed May 22, 2026, 8:07 PM

@jon-bell jon-bell marked this pull request as ready for review May 22, 2026 20:02
@jon-bell
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
supabase/functions/cli/commands/assessment.ts (1)

1373-1384: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Fix test_output_max_bytes truncation to use UTF-8 byte length (not string.length) in assessment.ts (supabase/functions/cli/commands/assessment.ts:1373-1384)

row.output is a string | null, but the code uses output.length and output.slice(0, outputMaxBytes), which count UTF-16 code units—not UTF-8 bytes. This makes both:

  • record.output_truncated decision incorrect for non-ASCII output
  • record.output_full_bytes = output.length incorrect

Truncate by UTF-8 byte length (e.g., iterate with TextEncoder/TextDecoder or equivalent) and set output_full_bytes to the UTF-8 byte length.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/cli/commands/assessment.ts` around lines 1373 - 1384, The
truncation logic in assessment.ts currently uses string.length and slice which
count UTF-16 code units; change it to compute the UTF-8 byte length of
row.output (use TextEncoder to get a Uint8Array), set record.output_full_bytes
to that byte length, and when output exceeds outputMaxBytes truncate by UTF-8
bytes (slice the encoded bytes to outputMaxBytes and decode back to a string
with TextDecoder) so record.output contains a properly truncated UTF-8-safe
string and record.output_truncated is set accordingly; update the branches that
reference output, outputMaxBytes, record.output, record.output_truncated, and
record.output_full_bytes to use this byte-based approach.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/commands/assessment/export.ts`:
- Around line 520-530: The pagination loop using batchIndex and the returned
next cursor (from streamAssignmentSection ->
page.endRecord.next_score_review_batch_index) must guard against non-advancing
cursors; update the loop to break (or throw) when next is not a number OR when
next <= batchIndex to prevent infinite loops and repeated edge calls. Do the
same change for the sibling loops that read next_answer_review_batch_index /
other next_*_batch_index cursors (the loops that call mergeBuckets,
addPageCounts, assertExpectedCount), i.e. only assign batchIndex = next when
typeof next === "number" && next > batchIndex, otherwise exit the loop
(optionally log a warning).

In `@supabase/functions/cli/commands/assessment.ts`:
- Around line 758-773: The switch over section (derived from params.section in
assessment.ts) currently treats unknown values as "all" via the default case,
which causes typos like "score" to run exportAssignmentAll; change this to
explicitly reject invalid section values by validating params.section (or the
computed const section) against the allowed AssignmentExportSection values and
replacing the switch default with logic that throws/returns a 400 Bad Request
(or calls the existing request-error helper) when the value is not one of
"meta","submissions","scores","tests","engagement","all"; update the switch in
the same function to only handle the known cases (exportAssignmentMeta,
exportAssignmentSubmissions, exportAssignmentScores, exportAssignmentTests,
exportAssignmentEngagement, exportAssignmentAll) and ensure unknown inputs
produce an immediate error response instead of falling back to
exportAssignmentAll.
- Around line 454-468: slimExtraDataForExport currently destructures prompt and
result into _prompt/_result but leaves them unused which trips
`@typescript-eslint/no-unused-vars`; after the destructuring in
slimExtraDataForExport (where llmObj and { prompt: _prompt, result: _result,
...llmRest } are declared) explicitly mark those vars as used by adding short
no-op references (for example: void _prompt; void _result;) before the return so
ESLint stops complaining while preserving the existing shape logic for llmObj
and llmRest.

In `@tests/unit/assessment-export-tokenization.test.ts`:
- Line 13: Update the import statement in the test to use the project-root alias
instead of the relative path: replace the relative import that currently pulls
createTokenizer, generateRandomSalt, and base32 from
"../../supabase/functions/cli/utils/tokenization" with the alias import from
"`@/supabase/functions/cli/utils/tokenization`" so the test imports
createTokenizer, generateRandomSalt, and base32 via the "`@/`..." alias.

---

Outside diff comments:
In `@supabase/functions/cli/commands/assessment.ts`:
- Around line 1373-1384: The truncation logic in assessment.ts currently uses
string.length and slice which count UTF-16 code units; change it to compute the
UTF-8 byte length of row.output (use TextEncoder to get a Uint8Array), set
record.output_full_bytes to that byte length, and when output exceeds
outputMaxBytes truncate by UTF-8 bytes (slice the encoded bytes to
outputMaxBytes and decode back to a string with TextDecoder) so record.output
contains a properly truncated UTF-8-safe string and record.output_truncated is
set accordingly; update the branches that reference output, outputMaxBytes,
record.output, record.output_truncated, and record.output_full_bytes to use this
byte-based approach.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 87d5ec32-d86b-420a-987e-d84f6df2646f

📥 Commits

Reviewing files that changed from the base of the PR and between 8bcd86b and d1a74b9.

📒 Files selected for processing (8)
  • cli/commands/assessment/deanonymize.ts
  • cli/commands/assessment/export.ts
  • cli/utils/transientRetry.ts
  • supabase/functions/cli/commands/assessment.ts
  • supabase/functions/cli/utils/tokenization.ts
  • supabase/migrations/20260522143000_assessment_export_pepper.sql
  • tests/unit/assessment-export-tokenization.test.ts
  • tests/unit/cli-transient-retry.test.ts

Comment on lines +520 to +530
for (let batchIndex = 0; ; ) {
const page = await streamAssignmentSection(baseParams, slug, "scores", {
score_review_batch_index: batchIndex
});
mergeBuckets(page.buckets);
addPageCounts(page.endRecord);
assertExpectedCount(page.endRecord, "scores", page.buckets.score!.length);
const next = page.endRecord.next_score_review_batch_index;
if (typeof next !== "number") break;
batchIndex = next;
}
Copy link
Copy Markdown
Contributor

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

Guard pagination cursors against non-advancing next_*_batch_index values.

At Line 528, Line 541, and Line 555, the loop only checks whether next is a number. If the server returns the same (or lower) batch index, these loops can run indefinitely and repeatedly hit the edge function.

Proposed fix
   for (let batchIndex = 0; ; ) {
     const page = await streamAssignmentSection(baseParams, slug, "scores", {
       score_review_batch_index: batchIndex
     });
@@
     const next = page.endRecord.next_score_review_batch_index;
     if (typeof next !== "number") break;
+    if (next <= batchIndex) {
+      throw new CLIError(
+        `assignment ${slug}: non-advancing next_score_review_batch_index (${next}) after ${batchIndex}`
+      );
+    }
     batchIndex = next;
   }
@@
   for (let batchIndex = 0; ; ) {
     const page = await streamAssignmentSection(baseParams, slug, "tests", {
       test_submission_batch_index: batchIndex
     });
@@
     const next = page.endRecord.next_test_submission_batch_index;
     if (typeof next !== "number") break;
+    if (next <= batchIndex) {
+      throw new CLIError(
+        `assignment ${slug}: non-advancing next_test_submission_batch_index (${next}) after ${batchIndex}`
+      );
+    }
     batchIndex = next;
   }
@@
   for (let batchIndex = 0; ; ) {
     const page = await streamAssignmentSection(baseParams, slug, "engagement", {
       engagement_submission_batch_index: batchIndex
     });
@@
     const next = page.endRecord.next_engagement_submission_batch_index;
     if (typeof next !== "number") break;
+    if (next <= batchIndex) {
+      throw new CLIError(
+        `assignment ${slug}: non-advancing next_engagement_submission_batch_index (${next}) after ${batchIndex}`
+      );
+    }
     batchIndex = next;
   }

Also applies to: 533-543, 546-557

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/commands/assessment/export.ts` around lines 520 - 530, The pagination
loop using batchIndex and the returned next cursor (from streamAssignmentSection
-> page.endRecord.next_score_review_batch_index) must guard against
non-advancing cursors; update the loop to break (or throw) when next is not a
number OR when next <= batchIndex to prevent infinite loops and repeated edge
calls. Do the same change for the sibling loops that read
next_answer_review_batch_index / other next_*_batch_index cursors (the loops
that call mergeBuckets, addPageCounts, assertExpectedCount), i.e. only assign
batchIndex = next when typeof next === "number" && next > batchIndex, otherwise
exit the loop (optionally log a warning).

Comment on lines +454 to +468
function slimExtraDataForExport(extra: unknown): unknown {
if (extra === null || extra === undefined || typeof extra !== "object") return extra ?? null;
const data = extra as Record<string, unknown>;
const llm = data.llm;
if (llm === null || llm === undefined || typeof llm !== "object") return extra;
const llmObj = llm as Record<string, unknown>;
const { prompt: _prompt, result: _result, ...llmRest } = llmObj;
return {
...data,
llm: {
...llmRest,
has_prompt: typeof llmObj.prompt === "string" && llmObj.prompt.length > 0,
has_result: typeof llmObj.result === "string" && llmObj.result.trim().length > 0
}
};
Copy link
Copy Markdown
Contributor

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

# Inspect the helper + surrounding logic
FILE="supabase/functions/cli/commands/assessment.ts"
sed -n '420,520p' "$FILE" | nl -ba | sed -n '1,120p'

# Find where handleAssignmentExport sets/validates `section`
rg -n "handleAssignmentExport|section" "$FILE" -S

# Find truncation logic using outputMaxBytes/output_full_bytes
rg -n "outputMaxBytes|output_full_bytes|truncate" "$FILE" -S

# Also inspect any test/output code paths nearby
sed -n '520,700p' "$FILE" | nl -ba | sed -n '1,200p'

Repository: pawtograder/platform

Length of output: 107


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="supabase/functions/cli/commands/assessment.ts"

echo "=== Code around slimExtraDataForExport (approx 430-510) ==="
sed -n '430,510p' "$FILE" | awk '{print NR ":" $0}'

echo "=== Code around handleAssignmentExport section logic (approx 380-520) ==="
sed -n '380,520p' "$FILE" | awk '{print NR ":" $0}'

echo "=== Search for section handling ==="
rg -n "handleAssignmentExport|section" "$FILE" -S || true

echo "=== Search for outputMaxBytes / output_full_bytes truncation ==="
rg -n "outputMaxBytes|output_full_bytes|truncate|full_bytes" "$FILE" -S || true

echo "=== Code around truncation vars (approx 520-650) ==="
sed -n '520,650p' "$FILE" | awk '{print NR ":" $0}'

Repository: pawtograder/platform

Length of output: 18272


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="supabase/functions/cli/commands/assessment.ts"

echo "=== Code around handleAssignmentExport switch (approx 730-820) ==="
sed -n '730,820p' "$FILE" | awk '{print NR ":" $0}'

echo "=== Code around truncation logic (approx 1200-1420) ==="
sed -n '1220,1420p' "$FILE" | awk '{print NR ":" $0}'

echo "=== Locate eslint no-unused-vars config ==="
ls -1 .eslintrc* eslint* package.json 2>/dev/null || true
rg -n "`@typescript-eslint/no-unused-vars`|no-unused-vars" .eslintrc* eslint* package.json . 2>/dev/null | head -n 50

echo "=== Find any rule override pattern for unused vars starting with '_' ==="
rg -n "argsIgnorePattern|varsIgnorePattern|ignoreRestSiblings|underscore" .eslintrc* eslint* package.json . 2>/dev/null | head -n 50

Repository: pawtograder/platform

Length of output: 13088


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show relevant eslint rule config
if [ -f eslint.config.mjs ]; then
  echo "=== eslint.config.mjs (relevant no-unused-vars rules) ==="
  rg -n "no-unused-vars|`@typescript-eslint/no-unused-vars`|varsIgnorePattern|argsIgnorePattern|ignoreRestSiblings" eslint.config.mjs
  sed -n '1,220p' eslint.config.mjs | awk '{print NR ":" $0}' | head -n 220
  # Also show around any matches
  MATCHES=$(rg -n "no-unused-vars|`@typescript-eslint/no-unused-vars`" eslint.config.mjs | cut -d: -f1 | head -n 20 || true)
  for L in $MATCHES; do
    echo "=== Context around eslint.config.mjs:$L ==="
    sed -n "$((L-10)),$((L+20))p" eslint.config.mjs | awk '{print NR ":" $0}' | head -n 60
  done
fi

FILE="supabase/functions/cli/commands/assessment.ts"
echo "=== Locate where handleAssignmentExport is wired / params schema ==="
rg -n "handler:\s*handleAssignmentExport|handleAssignmentExport\(|section\?|\bsection\b" "$FILE" -S

echo "=== Inspect around the CLI command definition (near handler) ==="
LINE=$(rg -n "handler:\s*handleAssignmentExport" "$FILE" -n | head -n 1 | cut -d: -f1 || true)
if [ -n "${LINE:-}" ]; then
  START=$((LINE-120))
  END=$((LINE+120))
  sed -n "${START},${END}p" "$FILE" | awk '{print NR ":" $0}'
fi

echo "=== Search for schema/validation (zod/parse) for section ==="
rg -n "zod|schema|parse|safeParse|validate|AssignmentExportSection|section.*(zod|parse|safeParse)" "$FILE" -S || true

Repository: pawtograder/platform

Length of output: 122


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== eslint.config.mjs length ==="
wc -l eslint.config.mjs 2>/dev/null || true

echo "=== eslint.config.mjs top 220 lines ==="
sed -n '1,220p' eslint.config.mjs 2>/dev/null | awk '{print NR ":" $0}' | head -n 220

echo "=== eslint.config.mjs full search for unused-vars config knobs ==="
rg -n "no-unused-vars|`@typescript-eslint/no-unused-vars`|varsIgnorePattern|argsIgnorePattern|ignoreRestSiblings|underscore|\\^_" eslint.config.mjs || true

echo "=== Search for section runtime validation in assessment.ts ==="
FILE="supabase/functions/cli/commands/assessment.ts"
rg -n "safeParse|zod|schema|parse\\(|validate\\(|enum\\(|AssignmentExportSection|section\\s*:" "$FILE" -S || true

echo "=== Find CLI option definitions around handleAssignmentExport wiring ==="
rg -n "handler:\s*handleAssignmentExport|handleAssignmentExport" "$FILE" -S
LINE=$(rg -n "handler:\s*handleAssignmentExport" "$FILE" -S | head -n 1 | cut -d: -f1)
if [ -n "${LINE:-}" ]; then
  START=$((LINE-160)); END=$((LINE+160));
  echo "=== assessment.ts around command registration (~$START-$END) ==="
  sed -n "${START},${END}p" "$FILE" | awk '{print NR ":" $0}' | head -n 260
fi

echo "=== Search for test_output_max_bytes docs/usage ==="
rg -n "test_output_max_bytes|outputMaxBytes|output_full_bytes|output\\.length" "$FILE" -S

Repository: pawtograder/platform

Length of output: 14749


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find any eslint rule config that mentions no-unused-vars / varsIgnorePattern / argsIgnorePattern
rg -n --hidden --glob '!.git/**' \
  "no-unused-vars|`@typescript-eslint/no-unused-vars`|varsIgnorePattern|argsIgnorePattern" \
  . -S || true

# 2) Inspect DEFAULT_TEST_OUTPUT_MAX_BYTES definition and surrounding docs
FILE="supabase/functions/cli/commands/assessment.ts"
rg -n "DEFAULT_TEST_OUTPUT_MAX_BYTES" "$FILE" -S
# print near the definition (top of file)
TOPLINE=$(rg -n "DEFAULT_TEST_OUTPUT_MAX_BYTES" "$FILE" -S | head -n 1 | cut -d: -f1)
START=$((TOPLINE-40)); END=$((TOPLINE+60))
sed -n "${START},${END}p" "$FILE" | awk '{print NR ":" $0}'

# 3) Show the exact helper line for destructuring to ensure the unused vars are truly _prompt/_result only.
sed -n '40,110p' "$FILE" | awk '{print NR ":" $0}' | cat
rg -n "slimExtraDataForExport|_prompt|_result|has_prompt|has_result" "$FILE" -S

Repository: pawtograder/platform

Length of output: 12614


Fix ESLint unused vars in slimExtraDataForExport (supabase/functions/cli/commands/assessment.ts, ~lines 454-468): _prompt/_result are destructured but never used, so @typescript-eslint/no-unused-vars trips on the destructuring line.

Proposed fix
 function slimExtraDataForExport(extra: unknown): unknown {
   if (extra === null || extra === undefined || typeof extra !== "object") return extra ?? null;
   const data = extra as Record<string, unknown>;
   const llm = data.llm;
   if (llm === null || llm === undefined || typeof llm !== "object") return extra;
   const llmObj = llm as Record<string, unknown>;
-  const { prompt: _prompt, result: _result, ...llmRest } = llmObj;
+  const llmRest = { ...llmObj };
+  const hasPrompt = typeof llmRest.prompt === "string" && llmRest.prompt.length > 0;
+  const hasResult = typeof llmRest.result === "string" && llmRest.result.trim().length > 0;
+  delete llmRest.prompt;
+  delete llmRest.result;
   return {
     ...data,
     llm: {
       ...llmRest,
-      has_prompt: typeof llmObj.prompt === "string" && llmObj.prompt.length > 0,
-      has_result: typeof llmObj.result === "string" && llmObj.result.trim().length > 0
+      has_prompt: hasPrompt,
+      has_result: hasResult
     }
   };
 }
📝 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
function slimExtraDataForExport(extra: unknown): unknown {
if (extra === null || extra === undefined || typeof extra !== "object") return extra ?? null;
const data = extra as Record<string, unknown>;
const llm = data.llm;
if (llm === null || llm === undefined || typeof llm !== "object") return extra;
const llmObj = llm as Record<string, unknown>;
const { prompt: _prompt, result: _result, ...llmRest } = llmObj;
return {
...data,
llm: {
...llmRest,
has_prompt: typeof llmObj.prompt === "string" && llmObj.prompt.length > 0,
has_result: typeof llmObj.result === "string" && llmObj.result.trim().length > 0
}
};
function slimExtraDataForExport(extra: unknown): unknown {
if (extra === null || extra === undefined || typeof extra !== "object") return extra ?? null;
const data = extra as Record<string, unknown>;
const llm = data.llm;
if (llm === null || llm === undefined || typeof llm !== "object") return extra;
const llmObj = llm as Record<string, unknown>;
const llmRest = { ...llmObj };
const hasPrompt = typeof llmRest.prompt === "string" && llmRest.prompt.length > 0;
const hasResult = typeof llmRest.result === "string" && llmRest.result.trim().length > 0;
delete llmRest.prompt;
delete llmRest.result;
return {
...data,
llm: {
...llmRest,
has_prompt: hasPrompt,
has_result: hasResult
}
};
}
🧰 Tools
🪛 ESLint

[error] 460-460: '_prompt' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)


[error] 460-460: '_result' is assigned a value but never used.

(@typescript-eslint/no-unused-vars)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/cli/commands/assessment.ts` around lines 454 - 468,
slimExtraDataForExport currently destructures prompt and result into
_prompt/_result but leaves them unused which trips
`@typescript-eslint/no-unused-vars`; after the destructuring in
slimExtraDataForExport (where llmObj and { prompt: _prompt, result: _result,
...llmRest } are declared) explicitly mark those vars as used by adding short
no-op references (for example: void _prompt; void _result;) before the return so
ESLint stops complaining while preserving the existing shape logic for llmObj
and llmRest.

Comment on lines +758 to +773
const section: AssignmentExportSection = params.section ?? "all";

switch (section) {
case "meta":
return exportAssignmentMeta(exportCtx);
case "submissions":
return exportAssignmentSubmissions(exportCtx);
case "scores":
return exportAssignmentScores(exportCtx, Math.max(0, params.score_review_batch_index ?? 0));
case "tests":
return exportAssignmentTests(exportCtx, Math.max(0, params.test_submission_batch_index ?? 0));
case "engagement":
return exportAssignmentEngagement(exportCtx, Math.max(0, params.engagement_submission_batch_index ?? 0));
case "all":
default:
return exportAssignmentAll(exportCtx);
Copy link
Copy Markdown
Contributor

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

Reject unknown section values instead of falling back to "all".

A typo like "score" currently lands in default and runs the monolithic export path. That changes behavior and can trigger the most expensive stream instead of returning a 400.

Proposed fix
   switch (section) {
     case "meta":
       return exportAssignmentMeta(exportCtx);
     case "submissions":
       return exportAssignmentSubmissions(exportCtx);
     case "scores":
       return exportAssignmentScores(exportCtx, Math.max(0, params.score_review_batch_index ?? 0));
     case "tests":
       return exportAssignmentTests(exportCtx, Math.max(0, params.test_submission_batch_index ?? 0));
     case "engagement":
       return exportAssignmentEngagement(exportCtx, Math.max(0, params.engagement_submission_batch_index ?? 0));
     case "all":
-    default:
       return exportAssignmentAll(exportCtx);
+    default:
+      throw new CLICommandError(`invalid section: ${String(params.section)}`, 400);
   }
 }
📝 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
const section: AssignmentExportSection = params.section ?? "all";
switch (section) {
case "meta":
return exportAssignmentMeta(exportCtx);
case "submissions":
return exportAssignmentSubmissions(exportCtx);
case "scores":
return exportAssignmentScores(exportCtx, Math.max(0, params.score_review_batch_index ?? 0));
case "tests":
return exportAssignmentTests(exportCtx, Math.max(0, params.test_submission_batch_index ?? 0));
case "engagement":
return exportAssignmentEngagement(exportCtx, Math.max(0, params.engagement_submission_batch_index ?? 0));
case "all":
default:
return exportAssignmentAll(exportCtx);
const section: AssignmentExportSection = params.section ?? "all";
switch (section) {
case "meta":
return exportAssignmentMeta(exportCtx);
case "submissions":
return exportAssignmentSubmissions(exportCtx);
case "scores":
return exportAssignmentScores(exportCtx, Math.max(0, params.score_review_batch_index ?? 0));
case "tests":
return exportAssignmentTests(exportCtx, Math.max(0, params.test_submission_batch_index ?? 0));
case "engagement":
return exportAssignmentEngagement(exportCtx, Math.max(0, params.engagement_submission_batch_index ?? 0));
case "all":
return exportAssignmentAll(exportCtx);
default:
throw new CLICommandError(`invalid section: ${String(params.section)}`, 400);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/functions/cli/commands/assessment.ts` around lines 758 - 773, The
switch over section (derived from params.section in assessment.ts) currently
treats unknown values as "all" via the default case, which causes typos like
"score" to run exportAssignmentAll; change this to explicitly reject invalid
section values by validating params.section (or the computed const section)
against the allowed AssignmentExportSection values and replacing the switch
default with logic that throws/returns a 400 Bad Request (or calls the existing
request-error helper) when the value is not one of
"meta","submissions","scores","tests","engagement","all"; update the switch in
the same function to only handle the known cases (exportAssignmentMeta,
exportAssignmentSubmissions, exportAssignmentScores, exportAssignmentTests,
exportAssignmentEngagement, exportAssignmentAll) and ensure unknown inputs
produce an immediate error response instead of falling back to
exportAssignmentAll.

* produce identical tokens for the same (kind, raw_id).
*/

import { createTokenizer, generateRandomSalt, base32 } from "../../supabase/functions/cli/utils/tokenization";
Copy link
Copy Markdown
Contributor

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Show the import line in the target test file
echo "---- File snippet: tests/unit/assessment-export-tokenization.test.ts (lines ~1-30) ----"
nl -ba tests/unit/assessment-export-tokenization.test.ts | sed -n '1,40p'

# 2) Find tsconfig path alias configuration for `@/`*
echo "---- tsconfig/jsconfig paths for `@/`* ----"
rg -n --hidden --glob 'tsconfig*.json' --glob 'jsconfig*.json' '"`@/`\*"' -S . || true
rg -n --hidden --glob 'tsconfig*.json' --glob 'jsconfig*.json' '"`@/`*"' -S . || true
rg -n --hidden --glob 'tsconfig*.json' --glob 'jsconfig*.json' 'paths' -S tsconfig*.json jsconfig*.json || true

# 3) Check existing imports from supabase/functions/cli/utils/tokenization across the repo
echo "---- Usages of createTokenizer/generateRandomSalt/base32 import ----"
rg -n "supabase/functions/cli/utils/tokenization" -S . || true

# 4) Check for `@/` usage patterns in tests to see if this convention is actually followed
echo "---- Example `@/`* imports in tests (up to 50 matches) ----"
rg -n "from \"`@/`" -S tests | head -n 50 || true

# 5) Check whether other imports from the same supabase/functions directory use relative paths or `@/`
echo "---- Imports from supabase/functions/* (relative vs `@/`), limited to tests ----"
rg -n "(from \"\\.{1,2}/supabase/|from \"`@/supabase/`)" -S tests || true

Repository: pawtograder/platform

Length of output: 213


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- File snippet: tests/unit/assessment-export-tokenization.test.ts (lines 1-40) ----"
cat -n tests/unit/assessment-export-tokenization.test.ts | sed -n '1,40p'

echo "---- tsconfig/jsconfig paths for `@/`* (searching) ----"
rg -n --hidden --glob 'tsconfig*.json' --glob 'jsconfig*.json' '"`@/`\*"' -S . || true
rg -n --hidden --glob 'tsconfig*.json' '\"`@/`\\*\"|\"@\\*\"|paths' -S . || true
rg -n --hidden --glob 'tsconfig*.json' --glob 'jsconfig*.json' '"`@/`\*"' -S tsconfig*.json jsconfig*.json 2>/dev/null || true

echo "---- Usages of supabase/functions/cli/utils/tokenization import ----"
rg -n "supabase/functions/cli/utils/tokenization" -S . || true

echo "---- Example `@/`* imports in tests (up to 50 matches) ----"
rg -n "from \"`@/`" -S tests | head -n 50 || true

echo "---- Imports from supabase/functions/* (relative vs `@/`), limited to tests ----"
rg -n "(from \"\\.{1,2}/supabase/|from \"`@/supabase/`)" -S tests || true

Repository: pawtograder/platform

Length of output: 8703


Use @/ alias for this test import

Replace the relative import with the project-root alias:

import { createTokenizer, generateRandomSalt, base32 } from "`@/supabase/functions/cli/utils/tokenization`";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/assessment-export-tokenization.test.ts` at line 13, Update the
import statement in the test to use the project-root alias instead of the
relative path: replace the relative import that currently pulls createTokenizer,
generateRandomSalt, and base32 from
"../../supabase/functions/cli/utils/tokenization" with the alias import from
"`@/supabase/functions/cli/utils/tokenization`" so the test imports
createTokenizer, generateRandomSalt, and base32 via the "`@/`..." alias.

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.

2 participants