<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Akanksha Trehun</title>
    <description>The latest articles on DEV Community by Akanksha Trehun (@magic-peach).</description>
    <link>https://dev.to/magic-peach</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3909309%2F540870bc-2796-4ade-a0dd-68e2fce4e1c2.png</url>
      <title>DEV Community: Akanksha Trehun</title>
      <link>https://dev.to/magic-peach</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/magic-peach"/>
    <language>en</language>
    <item>
      <title>Week 16: The Editor That Learned When to Say No</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Thu, 01 Oct 2026 12:29:03 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-16-the-editor-that-learned-when-to-say-no-36c2</link>
      <guid>https://dev.to/magic-peach/week-16-the-editor-that-learned-when-to-say-no-36c2</guid>
      <description>&lt;h1&gt;
  
  
  Week 16: The Editor That Learned When to Say No
&lt;/h1&gt;

&lt;p&gt;With the testbench model and the runner both fixed and moving, the test case&lt;br&gt;
editor finally got its own review round this week. It found the same class&lt;br&gt;
of bug the model found last week, in the client side JSON this time instead&lt;br&gt;
of the server side validation, and fixing it properly meant redesigning a&lt;br&gt;
piece of the editor rather than patching around the symptom. A second,&lt;br&gt;
unrelated bug turned up in a completely different pull request, caught only&lt;br&gt;
because a setting this phase just added made an old code path reachable for&lt;br&gt;
the first time.&lt;/p&gt;
&lt;h2&gt;
  
  
  The overwrite that would have erased a title on every save
&lt;/h2&gt;

&lt;p&gt;First finding, on &lt;code&gt;Assignment#testbench_data=&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;testbench&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;build_testbench&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parsed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This replaces the entire &lt;code&gt;data&lt;/code&gt; hash with whatever the editor's JSON posts.&lt;br&gt;
The editor only ever authors &lt;code&gt;type&lt;/code&gt; and &lt;code&gt;groups&lt;/code&gt;. If an existing suite had&lt;br&gt;
any other top level key, a &lt;code&gt;title&lt;/code&gt;, say, something the simulator itself reads&lt;br&gt;
at &lt;code&gt;testbench.js:872&lt;/code&gt;, it survives right up until the next time an instructor&lt;br&gt;
edits an unrelated setting and saves the form. Then it is gone, not because&lt;br&gt;
anyone touched it, but because the write path never preserved what it was not&lt;br&gt;
explicitly given.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;tb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;testbench&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;build_testbench&lt;/span&gt;
&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;data&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Merge instead of replace. I wrote a regression test that seeds a suite with a&lt;br&gt;
&lt;code&gt;title&lt;/code&gt;, saves an unrelated change through the editor, and asserts the title&lt;br&gt;
survives, then deleted the merge to confirm the test catches the loss. It&lt;br&gt;
does.&lt;/p&gt;
&lt;h2&gt;
  
  
  The same binding bug, one layer up
&lt;/h2&gt;

&lt;p&gt;The second finding was the model's &lt;code&gt;groups[0]&lt;/code&gt; binding problem again, except&lt;br&gt;
this time the mismatched pins would come from the editor itself, from an&lt;br&gt;
instructor adding a second test case row with different inputs than the&lt;br&gt;
first:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;each row becomes its own group, but the simulator binds pins from&lt;br&gt;
&lt;code&gt;groups[0]&lt;/code&gt; only. a row with a different pin set than the first makes&lt;br&gt;
&lt;code&gt;group.outputs.find(...).results.push&lt;/code&gt; return undefined and throw.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Same root cause as last week, different code path producing the malformed&lt;br&gt;
suite. The fix is a shape comparison in the JavaScript controller, mirroring&lt;br&gt;
what the Ruby model already does server side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;sameShape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shape&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;inputs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;outputs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;side&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;side&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bitWidth&lt;/span&gt;&lt;span class="p"&gt;])));&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nf"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Submission blocks client side now if any row's pins do not match the first&lt;br&gt;
row's, with the same pattern used for incomplete rows: &lt;code&gt;preventDefault&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;stopPropagation&lt;/code&gt; because rails-ujs still disables the submit button on a&lt;br&gt;
merely prevented submit as it bubbles to its document level listener, show&lt;br&gt;
the error, stop.&lt;/p&gt;

&lt;p&gt;A third finding, a values regex that accepted any non-empty string instead&lt;br&gt;
of exact-width binary digits, was the exact bug from last week's model&lt;br&gt;
review, just not yet enforced in the form that produces the JSON in the&lt;br&gt;
first place. Tightened the same way: &lt;code&gt;[01]+&lt;/code&gt;, checked against the declared&lt;br&gt;
bit width.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why I did not try to merge the unsupported case in
&lt;/h2&gt;

&lt;p&gt;The harder finding was about suites the editor cannot fully represent:&lt;br&gt;
multi vector test cases, where one row of the JSON actually encodes several&lt;br&gt;
values per pin instead of one. My first fix tried to preserve these by&lt;br&gt;
tracking each group's original array position, splitting edited groups from&lt;br&gt;
untouched ones, and re-merging them back into the original order on save.&lt;br&gt;
It worked, but CodeRabbit found two more bugs in that merge logic within&lt;br&gt;
a day: the merge could still reorder an unsupported group relative to an&lt;br&gt;
edited one, and it silently rewrote the suite's &lt;code&gt;type&lt;/code&gt; from &lt;code&gt;seq&lt;/code&gt; to &lt;code&gt;comb&lt;/code&gt;&lt;br&gt;
on every save because the serialization always hardcoded &lt;code&gt;comb&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two fixes for a problem that a different design does not have in the first&lt;br&gt;
place made me stop and reconsider the approach rather than the code. The&lt;br&gt;
editor cannot safely round trip a multi vector suite. Trying to merge it&lt;br&gt;
back together precisely is solving a harder problem than the feature needs:&lt;br&gt;
tracking position, preserving type, reconciling two sources of truth. The&lt;br&gt;
simpler rule is to never let the editor touch what it cannot fully&lt;br&gt;
represent. If any group in a suite has more than one value per pin, the&lt;br&gt;
whole editor goes read only for that assignment, with a note saying why,&lt;br&gt;
instead of rendering an editable interface that would corrupt the suite the&lt;br&gt;
moment someone saved through it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_case_editable?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;assignment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;test_case_groups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;assignment&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;all?&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"n"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;all?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"values"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"outputs"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;all?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"values"&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This deleted the position tracking, the merge-back logic, and every test&lt;br&gt;
written against it, and replaced all of it with one predicate and an&lt;br&gt;
&lt;code&gt;if/else&lt;/code&gt; in the view. The line count dropped by about sixty lines for a&lt;br&gt;
bigger guarantee: nothing this editor cannot represent can be silently&lt;br&gt;
damaged by it, because it is never editable in the first place.&lt;/p&gt;
&lt;h2&gt;
  
  
  Getting back under budget without cutting a fix
&lt;/h2&gt;

&lt;p&gt;This phase holds pull requests to roughly two hundred hand written lines.&lt;br&gt;
Chasing every one of these findings, plus the accessibility fixes an&lt;br&gt;
automated check flagged separately, pushed this diff past four hundred at&lt;br&gt;
its peak. Cutting it back down meant finding the difference between a line&lt;br&gt;
that is load bearing and a line that is just verbose.&lt;/p&gt;

&lt;p&gt;Consolidating five near identical validation specs into one table driven&lt;br&gt;
test recovered real lines without losing a single case. Dropping a helper&lt;br&gt;
spec file for a follow up PR, per this phase's own stated rule about&lt;br&gt;
splitting spec edge cases rather than thinning validation, recovered more.&lt;br&gt;
The read only redesign recovered the most of all, because it deleted code&lt;br&gt;
that existed to solve a problem a different design does not have. What did&lt;br&gt;
not move: the testbench model's shape and label validation, the values&lt;br&gt;
regex, the malformed pin token check. Those stayed exactly as strict as the&lt;br&gt;
reviews asked for, because a line count is a budget for how a feature is&lt;br&gt;
built, not a ceiling on whether a reported bug gets fixed.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bug that was always possible and just became reachable
&lt;/h2&gt;

&lt;p&gt;A different pull request from this same run, the one adding autograde&lt;br&gt;
settings like max attempts, got a review comment that had nothing to do with&lt;br&gt;
any of the new columns:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;when a mentor submits &lt;code&gt;max_attempts: 0&lt;/code&gt;, the new validation rejects&lt;br&gt;
&lt;code&gt;@assignment.update&lt;/code&gt;. the action renders &lt;code&gt;:edit&lt;/code&gt;, but&lt;br&gt;
&lt;code&gt;after_action :check_reopening_status&lt;/code&gt; still runs. if the assignment has a&lt;br&gt;
forked submission, &lt;code&gt;Assignment#check_reopening_status&lt;/code&gt; can move its&lt;br&gt;
assignment association and destroy the fork despite the failed update.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;after_action&lt;/code&gt; callbacks run regardless of whether the action they follow&lt;br&gt;
succeeded. This one always had that property. It just never mattered before,&lt;br&gt;
because almost nothing about updating an assignment used to fail validation.&lt;br&gt;
Adding a &lt;code&gt;max_attempts&lt;/code&gt; column with an actual numericality check gave&lt;br&gt;
mentors, for the first time, an easy way to submit a rejected update, which&lt;br&gt;
is the one condition this pre-existing callback was never guarded against.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_reopening_status&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@assignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;

  &lt;span class="vi"&gt;@assignment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check_reopening_status&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines. I reverted them and reran the new regression test to confirm a&lt;br&gt;
rejected update really does destroy a student's forked submission without&lt;br&gt;
the guard, which it does, then restored the fix and confirmed it survives.&lt;br&gt;
The bug was not introduced this phase. The conditions for triggering it&lt;br&gt;
were.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the project stands
&lt;/h2&gt;

&lt;p&gt;Three pull requests carry review fixes now instead of open findings: the&lt;br&gt;
testbench model, the runner, and the test case editor, the last one smaller&lt;br&gt;
and safer than when it went up. The autograde settings pull request gained&lt;br&gt;
an unrelated but real fix along the way. Underneath all of it, the actual&lt;br&gt;
grading pipeline, a result model that snapshots the suite it ran against&lt;br&gt;
rather than pointing at the live one, a job that calls the runner and&lt;br&gt;
respects max attempts, a mapper from raw score to whatever scale the&lt;br&gt;
assignment actually uses, and a results page that redacts hidden test cases&lt;br&gt;
unless an instructor has chosen to reveal them, went up this week as well.&lt;/p&gt;

&lt;p&gt;Next week: watching whether any of these five pull requests actually merge,&lt;br&gt;
and what the first one to land changes for everything still stacked behind&lt;br&gt;
it.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>circuitverse</category>
      <category>opensource</category>
      <category>selflearning</category>
    </item>
    <item>
      <title>Week 15: What the Simulator Actually Binds</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Thu, 01 Oct 2026 12:26:33 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-15-what-the-simulator-actually-binds-1l5i</link>
      <guid>https://dev.to/magic-peach/week-15-what-the-simulator-actually-binds-1l5i</guid>
      <description>&lt;h1&gt;
  
  
  Week 15: What the Simulator Actually Binds
&lt;/h1&gt;

&lt;p&gt;&lt;code&gt;#7746&lt;/code&gt; merged clean, rebase and all, closing out last week's story. That&lt;br&gt;
freed the week for the thing I actually promised: picking the test case&lt;br&gt;
editor back up. Except the editor cannot be evaluated on its own terms until&lt;br&gt;
its two dependencies, the testbench model (&lt;code&gt;#7836&lt;/code&gt;) and the runner (&lt;code&gt;#7906&lt;/code&gt;),&lt;br&gt;
are actually mergeable, and both came back from review this week with&lt;br&gt;
findings that needed fixing before either could move.&lt;/p&gt;
&lt;h2&gt;
  
  
  A label that is not a string
&lt;/h2&gt;

&lt;p&gt;CodeRabbit's first flag on the testbench model was on the signal validator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_signal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"every signal needs a label"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;present?&lt;/code&gt; rejects &lt;code&gt;nil&lt;/code&gt;, empty strings, and whitespace-only strings. It does&lt;br&gt;
not reject &lt;code&gt;1&lt;/code&gt;. A JSON body with &lt;code&gt;"label": 1&lt;/code&gt; sails through this check, gets&lt;br&gt;
stored, and later reaches the simulator, which calls &lt;code&gt;.trim()&lt;/code&gt; on every&lt;br&gt;
signal's label while binding pins to the circuit. Numbers do not have a&lt;br&gt;
&lt;code&gt;.trim()&lt;/code&gt; method in JavaScript. The failure is not "this test case behaves&lt;br&gt;
oddly," it is a &lt;code&gt;TypeError&lt;/code&gt; that kills the run.&lt;/p&gt;

&lt;p&gt;The fix is one clause:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"every signal needs a label"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small, but it is the kind of small that only shows up if you trace the value&lt;br&gt;
forward into the file that actually consumes it, rather than stopping at "the&lt;br&gt;
model validates its own shape."&lt;/p&gt;
&lt;h2&gt;
  
  
  The binding a human reviewer caught that a linter would not
&lt;/h2&gt;

&lt;p&gt;The second finding was not from CodeRabbit. A reviewer left this on the same&lt;br&gt;
file:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;nothing checks that later groups use the same input/output labels as&lt;br&gt;
&lt;code&gt;groups[0]&lt;/code&gt;. the simulator binds pins from &lt;code&gt;groups[0]&lt;/code&gt; only&lt;br&gt;
(testbench.js:818-823) and then does &lt;code&gt;inputs[label].state&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;group.outputs.find(...).results.push&lt;/code&gt; per group, so a suite that passes&lt;br&gt;
this validator can throw a TypeError mid-run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I went and read &lt;code&gt;testbench.js&lt;/code&gt; before touching anything, because the claim is&lt;br&gt;
specific enough to verify rather than trust. It is exactly right:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;bindIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
    &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;dataInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;dataInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(...);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only &lt;code&gt;data.groups[0]&lt;/code&gt; ever gets read here. Every other group is bound&lt;br&gt;
against whatever &lt;code&gt;bindIO&lt;/code&gt; built from the first one. If group two declares an&lt;br&gt;
input labeled &lt;code&gt;c&lt;/code&gt; that group one never had, &lt;code&gt;inputs['c']&lt;/code&gt; is &lt;code&gt;undefined&lt;/code&gt;, and&lt;br&gt;
&lt;code&gt;setInputValues&lt;/code&gt; dies trying to set &lt;code&gt;.state&lt;/code&gt; on it. The model's own&lt;br&gt;
validation, one group at a time, had no way to catch this, because the bug is&lt;br&gt;
not in any single group, it is in the relationship between groups.&lt;/p&gt;

&lt;p&gt;The fix compares each group after the first against the first's shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;reference&lt;/span&gt;

  &lt;span class="n"&gt;shape&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"label"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"bitWidth"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;next&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

  &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;side&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; must match the first group's signals"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Values that are the right length and the wrong alphabet
&lt;/h2&gt;

&lt;p&gt;Same reviewer, same file, a second finding: values were only length checked&lt;br&gt;
against the case count, never checked for actually being binary digits. The&lt;br&gt;
simulator feeds inputs through &lt;code&gt;parseInt(v, 2)&lt;/code&gt; and compares outputs against&lt;br&gt;
&lt;code&gt;dec2bin(value, bitWidth)&lt;/code&gt; as strings. &lt;code&gt;[0, 1]&lt;/code&gt; as integers, &lt;code&gt;"2"&lt;/code&gt; on a one&lt;br&gt;
bit pin, all of it passes the old check and then fails every single case at&lt;br&gt;
run time, silently, which reads to a student as "I got every case wrong"&lt;br&gt;
rather than "the test data itself is malformed."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;valid_values?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Integer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\A[01]{&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;}\z/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Exact width, exact alphabet, exact type. I added a regression case for a&lt;br&gt;
numeric label, one for wrong-width values, and one for a second group with a&lt;br&gt;
mismatched pin set, then reverted each fix in turn to confirm its matching&lt;br&gt;
test actually fails without it. All three did. A test that cannot fail is&lt;br&gt;
not a test, it is a comment that happens to run.&lt;/p&gt;
&lt;h2&gt;
  
  
  The runner's two findings were a different flavor entirely
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;#7906&lt;/code&gt;, the runner, got two findings that had nothing to do with the shape&lt;br&gt;
of any data structure:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;with this fallback a prod box missing &lt;code&gt;SIMULATOR_RUNNER_URL&lt;/code&gt; silently posts&lt;br&gt;
student circuits to its own localhost and every run just shows as&lt;br&gt;
"unreachable".&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;endpoint&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SIMULATOR_RUNNER_URL'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;DEFAULT_URL&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/run"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default was meant for local development convenience and quietly doubled&lt;br&gt;
as a production misconfiguration that fails silent instead of loud. Dropping&lt;br&gt;
the fallback entirely, &lt;code&gt;ENV.fetch('SIMULATOR_RUNNER_URL')&lt;/code&gt; with no second&lt;br&gt;
argument, means a missing variable raises immediately instead of routing&lt;br&gt;
every grading attempt to a service that was never there.&lt;/p&gt;

&lt;p&gt;The second finding was that the request carries no authentication at all,&lt;br&gt;
so anything that can reach the runner can make it simulate arbitrary&lt;br&gt;
circuits. I added a bearer token, &lt;code&gt;ENV.fetch('SIMULATOR_RUNNER_TOKEN')&lt;/code&gt;, sent&lt;br&gt;
on every request. The runner service on the other end still needs to&lt;br&gt;
actually check it, which is a different pull request's job, but the caller&lt;br&gt;
no longer has an excuse not to send one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two different ways of being right
&lt;/h2&gt;

&lt;p&gt;The testbench findings came from reading JavaScript nobody asked the model&lt;br&gt;
to know about. The runner findings came from reading the word "fallback" and&lt;br&gt;
asking what happens when the thing it falls back to is wrong. Neither is&lt;br&gt;
something a type checker or a schema validator would have caught on its&lt;br&gt;
own; both needed someone to actually trace a value from where it is written&lt;br&gt;
to where it is read. That is becoming the throughline of this whole review&lt;br&gt;
cycle: static analysis catches shapes, a reader who follows the data catches&lt;br&gt;
contracts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the project stands
&lt;/h2&gt;

&lt;p&gt;Both the testbench model and the runner have their review fixes pushed, all&lt;br&gt;
cases verified failing without their fix and passing with it. The test case&lt;br&gt;
editor is still sitting locally, unstacked, waiting for these two to actually&lt;br&gt;
merge before its own compare page can show one clean commit instead of three&lt;br&gt;
borrowed ones.&lt;/p&gt;

&lt;p&gt;Next week: the editor's own review round, which turns out to have found the&lt;br&gt;
same class of bug in the client side JSON that this week found in the model,&lt;br&gt;
plus one unrelated bug that only became reachable because of a setting this&lt;br&gt;
phase never had before.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>circuitverse</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>Week 14: The Approved Pull Request That Stopped Passing</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Tue, 08 Sep 2026 17:38:43 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-14-the-approved-pull-request-that-stopped-passing-eln</link>
      <guid>https://dev.to/magic-peach/week-14-the-approved-pull-request-that-stopped-passing-eln</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7746" rel="noopener noreferrer"&gt;#7746&lt;/a&gt;, the OIDC login&lt;br&gt;
initiation, has been approved and waiting since week 10. This week I went back&lt;br&gt;
to actually merge it and found its test suite failing on an error that had&lt;br&gt;
nothing to do with anything I remembered writing.&lt;/p&gt;
&lt;h2&gt;
  
  
  A NameError in code I had already shipped
&lt;/h2&gt;

&lt;p&gt;CI was red across the whole &lt;code&gt;oidc_login&lt;/code&gt; context, ten examples, all the same&lt;br&gt;
failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NameError:
  uninitialized constant LtiController::LTI_STATE_PURPOSE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That should have been impossible. &lt;code&gt;oidc_login&lt;/code&gt; and &lt;code&gt;lti_state_verifier&lt;/code&gt; both&lt;br&gt;
reference &lt;code&gt;LTI_STATE_PURPOSE&lt;/code&gt;, and the request spec references&lt;br&gt;
&lt;code&gt;LtiController::LTI_STATE_PURPOSE&lt;/code&gt; directly to verify the state it gets back.&lt;br&gt;
If that constant had never existed, this whole context should have failed the&lt;br&gt;
day it was written, back in week 10. It did not. Something had removed a&lt;br&gt;
constant that used to be there.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tracing it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;git log -p&lt;/code&gt; on the controller file gave a clean timeline. The constants first&lt;br&gt;
appear on 5 August, in the commit that adds &lt;code&gt;oidc_login&lt;/code&gt; itself, with a comment&lt;br&gt;
explaining why the state is signed rather than kept in the session. They survive&lt;br&gt;
the review fixes from week 10. They even survive a rough merge from master on 9&lt;br&gt;
August that briefly left the file unparseable, a splice that dropped the closing&lt;br&gt;
&lt;code&gt;end&lt;/code&gt; of &lt;code&gt;oidc_login&lt;/code&gt; and spliced two of master's filter lines into the method&lt;br&gt;
body. The very next commit repairs that, and its diff still shows the constants&lt;br&gt;
sitting untouched at the top of the class, comment and all.&lt;/p&gt;

&lt;p&gt;The actual disappearance happened somewhere I was not looking. On 21 August, a&lt;br&gt;
different contributor merged a repository wide change to master, adding&lt;br&gt;
&lt;code&gt;after_action :verify_authorized&lt;/code&gt; safety nets across dozens of controllers. On&lt;br&gt;
this controller, on master's copy of the file, that meant inserting&lt;br&gt;
&lt;code&gt;skip_after_action :verify_authorized&lt;/code&gt; as the very first line inside the class&lt;br&gt;
body. Master's copy at that point had no &lt;code&gt;LTI_STATE_PURPOSE&lt;/code&gt; constants, because&lt;br&gt;
none of this branch's work had merged into master yet.&lt;/p&gt;

&lt;p&gt;On 5 September I merged master into my branch again, to catch up before opening&lt;br&gt;
the merge. Both sides had inserted a new line in exactly the same spot, right&lt;br&gt;
after &lt;code&gt;class LtiController &amp;lt; ApplicationController&lt;/code&gt;: master's&lt;br&gt;
&lt;code&gt;skip_after_action&lt;/code&gt;, and my branch's constants with their comment. Diffing the&lt;br&gt;
file immediately before and after that merge shows exactly what happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt; class LtiController &amp;lt; ApplicationController
&lt;span class="gd"&gt;-  # The state is signed rather than kept in the session: the launch returns as a
-  # cross-site POST that a SameSite cookie would not survive, and the signature
-  # is what proves the launch answers an initiation we made.
-  LTI_STATE_PURPOSE = "lti.launch.state"
-  LTI_STATE_TTL = 5.minutes
&lt;/span&gt;&lt;span class="gi"&gt;+  skip_after_action :verify_authorized
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No conflict markers survive in the final commit, which is the part that makes&lt;br&gt;
this easy to miss. Two branches touching the same handful of lines usually forces&lt;br&gt;
a manual conflict resolution, and somewhere in resolving that one I kept&lt;br&gt;
master's line and let mine go, without noticing the branch had put something in&lt;br&gt;
that exact spot too. A merge that finishes clean does not look like something&lt;br&gt;
that needs a second look, which is exactly why this one went unnoticed for two&lt;br&gt;
weeks.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Both constants, reinstated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;LTI_STATE_PURPOSE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"lti_oidc_state"&lt;/span&gt;
&lt;span class="no"&gt;LTI_STATE_TTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The purpose string itself does not need to match the original literal, nothing&lt;br&gt;
outside this file ever reads it, only the two internal call sites and the spec&lt;br&gt;
need to agree on the same constant, which they now do by definition. The TTL&lt;br&gt;
does need to stay at five minutes, since the spec travels six minutes forward&lt;br&gt;
and checks the state is gone by then. I confirmed nothing else in the codebase&lt;br&gt;
referenced either constant, ran the project's RuboCop config against the change&lt;br&gt;
to be sure the addition was clean, and pushed. CI went from ten failures back to&lt;br&gt;
green across every job, not just the one that had been failing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this says about "approved and waiting"
&lt;/h2&gt;

&lt;p&gt;An approved pull request sitting unmerged is not a pull request that is done.&lt;br&gt;
It is a pull request whose last green check run is quietly getting further from&lt;br&gt;
the truth every time master moves underneath it, and nothing about the GitHub UI&lt;br&gt;
makes that visible. The checkmark from three weeks ago looks exactly like a&lt;br&gt;
checkmark from this morning.&lt;/p&gt;

&lt;p&gt;This is the same lesson as week 10's development database drift, just at a&lt;br&gt;
different layer. State that nobody actively re-verifies accumulates damage that&lt;br&gt;
stays invisible until something finally runs against it again. There, it was a&lt;br&gt;
migration nobody had rolled back. Here, it was a merge nobody had rerun. Both&lt;br&gt;
times the fix was cheap once found and the finding was the entire cost.&lt;/p&gt;

&lt;p&gt;The practical rule I am taking from this: before merging anything that has sat&lt;br&gt;
approved for more than a couple of weeks, rebase it against current master and&lt;br&gt;
rerun the suite first, rather than trusting a check that ran against a branch&lt;br&gt;
state that no longer exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the project stands
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;#7746&lt;/code&gt; is rebased, fixed, and green again, waiting on nothing but the merge&lt;br&gt;
button. &lt;code&gt;#7764&lt;/code&gt;, the access token exchange, is still open from last week. The&lt;br&gt;
testbench model and runner from two weeks ago are both under review. The test&lt;br&gt;
case editor is still finished and waiting locally.&lt;/p&gt;

&lt;p&gt;Next week: actually watching the login initiation merge, and picking the editor&lt;br&gt;
back up now that it has an access token to eventually authenticate against.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>opensource</category>
      <category>lti</category>
      <category>git</category>
    </item>
    <item>
      <title>Week 13: A Kid Only the Platform Can Read</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Tue, 08 Sep 2026 17:29:36 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-13-a-kid-only-the-platform-can-read-51hj</link>
      <guid>https://dev.to/magic-peach/week-13-a-kid-only-the-platform-can-read-51hj</guid>
      <description>&lt;p&gt;The access token work that jumped the queue last week went up as&lt;br&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7764" rel="noopener noreferrer"&gt;#7764&lt;/a&gt;. It took a&lt;br&gt;
security minded review to catch a bug that would have made every single token&lt;br&gt;
request fail, and a second reviewer's confusion, arriving weeks later, to remind&lt;br&gt;
me that a review thread can point at code that no longer exists.&lt;/p&gt;
&lt;h2&gt;
  
  
  What the tool needs before it can call anything
&lt;/h2&gt;

&lt;p&gt;Every LTI Advantage service call, posting a grade, reading a roster, is&lt;br&gt;
authenticated the same way. The tool signs a JWT assertion with its own private&lt;br&gt;
key, POSTs it to the platform's token endpoint under the OAuth 2.0 client&lt;br&gt;
credentials grant, and gets back a bearer token scoped to whatever it asked for.&lt;br&gt;
None of that is CircuitVerse specific. It is 1EdTech's Security Framework&lt;br&gt;
section 5.4.1 built on top of RFC 7523, the JWT profile for client assertions,&lt;br&gt;
which itself sits on RFC 6749's client credentials grant.&lt;/p&gt;

&lt;p&gt;Tokens are cached per deployment and per scope set, refreshed thirty seconds&lt;br&gt;
before they expire rather than exactly at expiry, so a request landing in that&lt;br&gt;
window does not race a token that is technically still valid when read and&lt;br&gt;
expired by the time it reaches the platform. A blank or non string&lt;br&gt;
&lt;code&gt;access_token&lt;/code&gt; in the response is refused instead of cached, and a response with&lt;br&gt;
no &lt;code&gt;expires_in&lt;/code&gt; at all gets no token stored, rather than a guessed default&lt;br&gt;
lifetime. Both of those came from reading the spec's failure modes rather than&lt;br&gt;
just its happy path.&lt;/p&gt;
&lt;h2&gt;
  
  
  The bug a reviewer caught before merge
&lt;/h2&gt;

&lt;p&gt;The first version signed the assertion like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;iss: &lt;/span&gt;&lt;span class="n"&gt;deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;sub: &lt;/span&gt;&lt;span class="n"&gt;deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="n"&gt;signing_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"RS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;kid: &lt;/span&gt;&lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;JWK&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signing_key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;kid&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A reviewer flagged the &lt;code&gt;kid&lt;/code&gt; within a day. &lt;code&gt;Lti::KeyManager&lt;/code&gt;, the class that&lt;br&gt;
publishes CircuitVerse's public key at &lt;code&gt;/lti/jwks&lt;/code&gt;, derives its key id from a&lt;br&gt;
&lt;code&gt;JSON::JWK&lt;/code&gt; thumbprint. &lt;code&gt;JWT::JWK.new(signing_key).kid&lt;/code&gt; computes a &lt;code&gt;kid&lt;/code&gt; using a&lt;br&gt;
different digest entirely. The two never agree, which means the assertion would&lt;br&gt;
have named a key that does not match anything the platform can find at our own&lt;br&gt;
published JWKS endpoint. Every token request would fail, not occasionally, every&lt;br&gt;
time, because the mismatch is structural rather than a bug that only shows up&lt;br&gt;
under some inputs.&lt;/p&gt;

&lt;p&gt;The fix reuses the exact computation &lt;code&gt;KeyManager&lt;/code&gt; already does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;kid_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signing_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;JWK&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;signing_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;thumbprint&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same input, the public key, same digest, &lt;code&gt;JSON::JWK&lt;/code&gt;'s thumbprint. I checked&lt;br&gt;
this by rereading &lt;code&gt;KeyManager#public_jwk&lt;/code&gt; line by line rather than trusting that&lt;br&gt;
two thumbprint calls with similar names must produce the same output, and they&lt;br&gt;
do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;public_jwk&lt;/span&gt;
  &lt;span class="n"&gt;jwk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;JSON&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;JWK&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;private_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;public_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;jwk&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:kid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="n"&gt;jwk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;thumbprint&lt;/span&gt;
  &lt;span class="n"&gt;jwk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;use: &lt;/span&gt;&lt;span class="s2"&gt;"sig"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;alg: &lt;/span&gt;&lt;span class="s2"&gt;"RS256"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A comment that arrived three weeks late, pointed at code that no longer existed
&lt;/h2&gt;

&lt;p&gt;The fix shipped within a day of the review comment. Three weeks later, a second&lt;br&gt;
reviewer reread the thread from the top and asked why I was "still" using&lt;br&gt;
&lt;code&gt;JWT::JWK.new(signing_key)&lt;/code&gt;, pointing straight at the original code block.&lt;/p&gt;

&lt;p&gt;GitHub pins a review comment to the diff position it was written against. Once&lt;br&gt;
that hunk changes, the thread gets marked "Outdated," but the stale code stays&lt;br&gt;
visible inline right where the comment is anchored, and the label is easy to&lt;br&gt;
miss if you are reading a long thread quickly rather than checking the Files&lt;br&gt;
Changed tab. The question was reasonable. The premise, that the flagged line&lt;br&gt;
was still in the branch, was not.&lt;/p&gt;

&lt;p&gt;The reply pointed at the fix commit by hash, quoted the current &lt;code&gt;kid_for&lt;/code&gt;&lt;br&gt;
method, and reread &lt;code&gt;KeyManager&lt;/code&gt;'s source alongside it to show the two&lt;br&gt;
computations are the same, rather than just asserting "it's fixed, trust me."&lt;br&gt;
That habit, rederiving a claim instead of restating it, is the same one from&lt;br&gt;
week 9's JWT validator work: a test that passes for the wrong reason is worse&lt;br&gt;
than no test, and an explanation that only restates the conclusion is worse than&lt;br&gt;
one that shows the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  A question with a better answer than "I just know"
&lt;/h2&gt;

&lt;p&gt;The same reviewer asked, on a different thread, how I knew which fields&lt;br&gt;
belonged in the token request body at all, and whether it was written down&lt;br&gt;
anywhere or just something I had picked up.&lt;/p&gt;

&lt;p&gt;It is written down, in three places that stack on each other. &lt;code&gt;grant_type&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;scope&lt;/code&gt; come from RFC 6749 section 4.4, the client credentials grant itself.&lt;br&gt;
&lt;code&gt;client_assertion&lt;/code&gt; and &lt;code&gt;client_assertion_type&lt;/code&gt; come from RFC 7523 section 2.2,&lt;br&gt;
the general mechanism for authenticating with a signed JWT instead of a shared&lt;br&gt;
secret. The specific URI value platforms expect for&lt;br&gt;
&lt;code&gt;client_assertion_type&lt;/code&gt;, and the requirement that the JWT's &lt;code&gt;iss&lt;/code&gt; and &lt;code&gt;sub&lt;/code&gt; both&lt;br&gt;
equal the tool's own client id, come from the IMS Security Framework, which is&lt;br&gt;
the layer that actually ties LTI Advantage to that OAuth mechanism.&lt;/p&gt;

&lt;p&gt;I answered with the three documents and the section numbers rather than a&lt;br&gt;
summary of what they say, because a summary is something to remember and a&lt;br&gt;
citation is something to reopen. Anyone auditing this code later, including me&lt;br&gt;
in six months, needs the second thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the project stands
&lt;/h2&gt;

&lt;p&gt;The access token pull request is open with both review threads addressed, the&lt;br&gt;
kid bug fixed and explained, and the spec provenance question answered with&lt;br&gt;
citations rather than recollection. Roster sync and grade passback can now, once&lt;br&gt;
this merges, actually authenticate the calls they have been assuming all along.&lt;/p&gt;

&lt;p&gt;Next week: going back to merge an older, already approved pull request, and&lt;br&gt;
finding out it had stopped passing while nobody was looking at it.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>opensource</category>
      <category>oauth</category>
      <category>jwt</category>
    </item>
    <item>
      <title>Week 12: Two Small Pull Requests</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Tue, 08 Sep 2026 17:26:16 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-12-two-small-pull-requests-1jgg</link>
      <guid>https://dev.to/magic-peach/week-12-two-small-pull-requests-1jgg</guid>
      <description>&lt;p&gt;Both pieces of the autograding groundwork from last week's "pipeline nobody could&lt;br&gt;
feed" post finally went up for review. Getting them out the door taught me more&lt;br&gt;
about the packaging rule I set for myself than the underlying feature did, and&lt;br&gt;
along the way I found a gap in my own merged code that rhymes uncomfortably with&lt;br&gt;
the one I found last week.&lt;/p&gt;
&lt;h2&gt;
  
  
  The rule I made myself follow
&lt;/h2&gt;

&lt;p&gt;Somewhere back in this phase I decided to open small pull requests one at a time&lt;br&gt;
and hold dependent branches locally until the parent merges, instead of stacking&lt;br&gt;
them or folding a dependency into one oversized diff. I have rejected my own&lt;br&gt;
compare pages twice for showing more than one commit, which is a strange thing to&lt;br&gt;
do to yourself but a useful one.&lt;/p&gt;

&lt;p&gt;The test case editor is the clearest reason why. Its branch holds a cherry pick&lt;br&gt;
of the testbench model's commit plus the editor's own commit, because the editor&lt;br&gt;
needs the model's table to exist to run its tests at all. Opening it now would&lt;br&gt;
show two commits on the compare page, and a reviewer would have to work out which&lt;br&gt;
one is actually new. So it stays local until the model merges, at which point I&lt;br&gt;
rebase, the borrowed commit drops out, and what is left is one clean diff over&lt;br&gt;
the editor's own files.&lt;/p&gt;
&lt;h2&gt;
  
  
  Which one first, and why it wasn't obvious
&lt;/h2&gt;

&lt;p&gt;That left a smaller decision: open the testbench model or the runner first.&lt;/p&gt;

&lt;p&gt;The runner's spec is deliberately built so it does not need the model to exist.&lt;br&gt;
It signs its double with &lt;code&gt;Struct.new(:data)&lt;/code&gt; instead of pulling in the real&lt;br&gt;
&lt;code&gt;Testbench&lt;/code&gt; factory, so the runner is green against master on its own. Nothing&lt;br&gt;
about it forces the model to land first.&lt;/p&gt;

&lt;p&gt;I opened the model first anyway, for three reasons that all point the same way.&lt;br&gt;
The test case editor is stacked on the model, not the runner, so unblocking the&lt;br&gt;
editor means merging the model first regardless. The model is also the smaller,&lt;br&gt;
easier diff: one migration, one class, one spec, no external HTTP contract to&lt;br&gt;
explain to a reviewer. And both branches touch &lt;code&gt;db/schema.rb&lt;/code&gt;, which is exactly&lt;br&gt;
the kind of file you do not want two open pull requests editing at once, since&lt;br&gt;
whichever merges second has to rebase past a version bump it did not cause.&lt;/p&gt;
&lt;h2&gt;
  
  
  What actually lives in the Testbench model
&lt;/h2&gt;

&lt;p&gt;One row per assignment, JSON in a &lt;code&gt;jsonb&lt;/code&gt; column, validated in three layers that&lt;br&gt;
mirror the shape of the data itself: suite, then group, then signal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;data_is_runnable&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"must be a testbench object"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_a?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Hash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"type must be comb or seq"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="no"&gt;TYPES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;include?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"must define a group"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;

  &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;validate_group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each group needs a case count and at least one input and one output. Each signal&lt;br&gt;
needs a label to bind to a circuit pin, a positive bit width, and exactly as many&lt;br&gt;
values as its group's case count. That last check is why validation lives in&lt;br&gt;
Ruby instead of a database constraint. A &lt;code&gt;jsonb&lt;/code&gt; column schema check can enforce&lt;br&gt;
shape, but it cannot easily say "this array's length must equal that other&lt;br&gt;
field's value," and that cross-field relationship is most of what makes a&lt;br&gt;
testbench actually runnable rather than merely well formed.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;assignment_id&lt;/code&gt; is unique at both levels, a database index and a model&lt;br&gt;
validation, because a race on create could otherwise slip a second row past a&lt;br&gt;
Ruby only check. &lt;code&gt;TYPES&lt;/code&gt; is closed to &lt;code&gt;comb&lt;/code&gt; and &lt;code&gt;seq&lt;/code&gt;, the two suite types the&lt;br&gt;
simulator can actually execute, so a suite claiming &lt;code&gt;verilog&lt;/code&gt; fails at save time&lt;br&gt;
instead of failing later, mid grade, in front of a student.&lt;/p&gt;
&lt;h2&gt;
  
  
  The runner, and the decision that already happened
&lt;/h2&gt;

&lt;p&gt;Most of the runner's design was decided last week, before a line of it existed.&lt;br&gt;
This week was writing it down.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;testbench&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
  &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;testbench&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One entrypoint. It posts the project's circuit JSON and the testbench's suite&lt;br&gt;
JSON to a sidecar service over HTTP, with timeouts split into connect, write,&lt;br&gt;
and read rather than one blanket number, because a hung connection should fail&lt;br&gt;
fast while an actual simulation run legitimately needs more time than the&lt;br&gt;
handshake. Every way this can go wrong, a bad status, an unreachable host, a&lt;br&gt;
body that is not JSON, a response with no groups, collapses into one&lt;br&gt;
&lt;code&gt;Autograder::Runner::RunnerError&lt;/code&gt;, so the grading job that calls this later only&lt;br&gt;
ever needs to rescue one thing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:passed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:groups&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;score&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zero?&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;passed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fdiv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The raw &lt;code&gt;groups&lt;/code&gt; array rides along inside the result even though only &lt;code&gt;score&lt;/code&gt;&lt;br&gt;
matters right now, because the results page a few pull requests from now needs&lt;br&gt;
to show which specific case failed, not just the fraction. The endpoint's host&lt;br&gt;
comes from &lt;code&gt;SIMULATOR_RUNNER_URL&lt;/code&gt;, following the same undocumented environment&lt;br&gt;
variable pattern the codebase already uses for &lt;code&gt;YOSYS_PATH&lt;/code&gt;, so anyone who has&lt;br&gt;
configured one external tool location here already knows how to configure this&lt;br&gt;
one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "next" turned out to mean
&lt;/h2&gt;

&lt;p&gt;Last week's post ended by promising the editor would land next. It has not, and&lt;br&gt;
writing the runner's pull request description is what changed my mind.&lt;/p&gt;

&lt;p&gt;I went back to reread the roster sync pull request from two weeks ago, the one&lt;br&gt;
that calls the platform's Names and Roles service, to check whether it needed&lt;br&gt;
anything from this new access token machinery. It does. So does grade passback.&lt;br&gt;
Both of those already assume an &lt;code&gt;Authorization: Bearer&lt;/code&gt; header carrying a token&lt;br&gt;
scoped to the right service, and until this week nothing in the codebase could&lt;br&gt;
produce one. I had built two pull requests that read from and write to platform&lt;br&gt;
services that require an OAuth exchange neither of them performs.&lt;/p&gt;

&lt;p&gt;This is the same shape of gap as last week's missing test case editor: a feature&lt;br&gt;
description satisfied on paper by code that quietly assumes a piece nobody has&lt;br&gt;
built yet. The difference is I caught this one myself, by rereading my own&lt;br&gt;
merged work with the new piece in mind, rather than having a mentor's evaluation&lt;br&gt;
catch it for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the project stands
&lt;/h2&gt;

&lt;p&gt;The testbench model and the runner are both open for review, in that order.&lt;br&gt;
Roster sync and grade passback are merged but cannot actually authenticate&lt;br&gt;
against a platform yet. The test case editor is finished and waiting locally for&lt;br&gt;
the model to merge.&lt;/p&gt;

&lt;p&gt;Next week: the access token exchange that both of those merged pull requests&lt;br&gt;
have been silently depending on.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>lti</category>
      <category>opensource</category>
      <category>dev</category>
    </item>
    <item>
      <title>Week 11: Welcome to my Midterm evaluation</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sun, 16 Aug 2026 06:06:06 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-11-welcome-to-my-midterm-evaluation-2l</link>
      <guid>https://dev.to/magic-peach/week-11-welcome-to-my-midterm-evaluation-2l</guid>
      <description>&lt;p&gt;Midterm evaluations ran from the 10th to the 14th. Deep linking and roster sync both&lt;br&gt;
shipped, two more pull requests merged, and then I sat down to write an honest account of&lt;br&gt;
where the project stands — which is how I found out that the plan I have been working&lt;br&gt;
from for a month builds a machine nobody can switch on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deep linking, end to end
&lt;/h2&gt;

&lt;p&gt;Two pull requests, two days apart.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7772" rel="noopener noreferrer"&gt;#7772&lt;/a&gt; handles the request&lt;br&gt;
side: a launch arrives with &lt;code&gt;LtiDeepLinkingRequest&lt;/code&gt; as its message type instead of&lt;br&gt;
&lt;code&gt;LtiResourceLinkRequest&lt;/code&gt;, and everything downstream has to route differently. The&lt;br&gt;
platform sends along &lt;code&gt;deep_linking_settings&lt;/code&gt; — where to return the response, whether&lt;br&gt;
multiple items are allowed, whether the platform wants a title back — and that has to be&lt;br&gt;
stashed for the response, which happens on a completely separate request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7777" rel="noopener noreferrer"&gt;#7777&lt;/a&gt; is what the instructor&lt;br&gt;
actually sees: a picker listing their circuits and assignments, with the selection posted&lt;br&gt;
back to the LMS as a signed JWT.&lt;/p&gt;

&lt;p&gt;The satisfying part is that neither pull request needed anything new from the protocol&lt;br&gt;
layer. The JWT signing is the key manager from week 4. The validation on the way in is&lt;br&gt;
the validator from week 9. Deep linking is a different &lt;em&gt;conversation&lt;/em&gt; with the platform,&lt;br&gt;
but it is the same handshake underneath, and by this point the handshake is just there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The roster
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7779" rel="noopener noreferrer"&gt;#7779&lt;/a&gt; reads a course roster&lt;br&gt;
over the Names and Roles service — pagination via &lt;code&gt;Link&lt;/code&gt; headers, roles parsed out of the&lt;br&gt;
IMS role URIs. &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7781" rel="noopener noreferrer"&gt;#7781&lt;/a&gt; turns&lt;br&gt;
that into group members.&lt;/p&gt;

&lt;p&gt;The interesting decision in the second one is how users get matched. Not on email — that&lt;br&gt;
was week 5's lesson, and the reason for it holds here too: a platform's privacy settings&lt;br&gt;
may mean no email arrives at all, and even when one does, it might belong to a different&lt;br&gt;
CircuitVerse account than the person clicking. The match is on &lt;code&gt;sub&lt;/code&gt; plus issuer, the&lt;br&gt;
same identity pair the launch signs the user in with. A roster import that mapped on&lt;br&gt;
email could silently add a stranger to a course.&lt;/p&gt;

&lt;p&gt;Members created this way get marked as LTI-synced, which matters for the drop-handling&lt;br&gt;
pull request that follows: when someone disappears from the LMS roster, only synced&lt;br&gt;
members get deactivated. Somebody who joined the CircuitVerse group directly should not&lt;br&gt;
be removed because they were never in Canvas to begin with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two merges
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7715" rel="noopener noreferrer"&gt;#7715&lt;/a&gt;, the JWT validator,&lt;br&gt;
merged on the 11th. &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7763" rel="noopener noreferrer"&gt;#7763&lt;/a&gt;,&lt;br&gt;
the resource link model, merged on the 12th. Both had been sitting approved for a while —&lt;br&gt;
the validator since the end of July.&lt;/p&gt;

&lt;p&gt;Five merged now: the 1.1 passback fix, the deployment model, the JWKS and tool&lt;br&gt;
configuration endpoints, the validator, and the resource link. Eight more open and&lt;br&gt;
waiting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The evaluation
&lt;/h2&gt;

&lt;p&gt;What the evaluation actually asked me to do was describe the state of the project, which&lt;br&gt;
sounds trivial and was not. Writing "here is what works" forces a different question than&lt;br&gt;
"here is what I built this week," and the two answers were not the same shape.&lt;/p&gt;

&lt;p&gt;So I went back to the three issues this project exists to close and read them line by&lt;br&gt;
line against what is in the repository. Not the proposal — the issues, as filed.&lt;/p&gt;

&lt;h2&gt;
  
  
  A pipeline nobody could feed
&lt;/h2&gt;

&lt;p&gt;The autograding phase of my plan had six pull requests: a test case model, a headless&lt;br&gt;
runner wrapper, a grading result, a job that runs on submission, a results page, and a&lt;br&gt;
push to the LMS gradebook. Student submits, autograder runs, grade lands in Canvas.&lt;/p&gt;

&lt;p&gt;Every one of those is about &lt;em&gt;executing&lt;/em&gt; test cases. Not one of them is about &lt;em&gt;creating&lt;/em&gt;&lt;br&gt;
test cases.&lt;/p&gt;

&lt;p&gt;The issue says it plainly — "a test case editor in the assignment settings UI where&lt;br&gt;
instructors can add, edit and delete test cases" — and I had somehow read that as&lt;br&gt;
satisfied by a database table. It is not. A model with no UI means the only way to define&lt;br&gt;
a test suite is the Rails console, which is not a feature an instructor can use. I had&lt;br&gt;
planned six pull requests that would ship a grading pipeline with no way to put anything&lt;br&gt;
into it.&lt;/p&gt;

&lt;p&gt;Reading further with that lens, four more gaps fell out. There was no visible/hidden flag&lt;br&gt;
on test cases, which both issues ask for. There was nothing for the autograde&lt;br&gt;
configuration — partial credit, allowed attempts, whether students see the cases before&lt;br&gt;
submitting. "Versioned with the assignment" had no plan behind it at all, and my model&lt;br&gt;
actively worked against it: one row per assignment, edited in place, so changing a suite&lt;br&gt;
would quietly invalidate every grade already produced from it.&lt;/p&gt;

&lt;p&gt;And the least visible one, which I only found by reading the &lt;code&gt;Grade&lt;/code&gt; model rather than&lt;br&gt;
remembering it: nothing in the plan mapped a score onto an assignment's grading scale.&lt;br&gt;
The autograder produces a fraction. &lt;code&gt;Grade&lt;/code&gt; validates against &lt;code&gt;letter&lt;/code&gt;, &lt;code&gt;percent&lt;/code&gt;, or&lt;br&gt;
&lt;code&gt;custom&lt;/code&gt;, and rejects &lt;code&gt;no_scale&lt;/code&gt; outright. Autograding an assignment with no grading&lt;br&gt;
scale would have failed at the last step, after the run, with the student watching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Twenty-four becomes twenty-seven
&lt;/h2&gt;

&lt;p&gt;Week 7's post was called "Twenty-Four Pull Requests." That plan is now twenty-seven, all&lt;br&gt;
three additions in the autograding phase: the test case editor, the autograde settings,&lt;br&gt;
and the grading-scale mapping — plus reworking the grading result to carry an attempt&lt;br&gt;
number and a snapshot, and folding hidden-case redaction into the results page.&lt;/p&gt;

&lt;p&gt;The snapshot is how "versioned with the assignment" gets solved, and it is the change I&lt;br&gt;
am happiest with. Rather than a version table with all the machinery that implies, each&lt;br&gt;
grading result stores a copy of the suite it ran against. Edit the suite afterwards and&lt;br&gt;
old results stay reproducible, because the thing that produced them is still sitting&lt;br&gt;
right there next to them.&lt;/p&gt;

&lt;p&gt;Three of the nine are written. The model and the runner are pushed; the editor is done&lt;br&gt;
and waiting locally, because it needs the model's table to exist and I would rather open&lt;br&gt;
one clean pull request after the first merges than one showing two commits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusing a format instead of inventing one
&lt;/h2&gt;

&lt;p&gt;The nicest thing I wrote this week is four words long.&lt;/p&gt;

&lt;p&gt;A test case needs a name, input pin values, expected output pin values, and a hidden&lt;br&gt;
flag. The simulator's testbench format already has &lt;em&gt;groups&lt;/em&gt; — a named collection of cases&lt;br&gt;
with input and output signals. My first instinct was to add fields: a &lt;code&gt;name&lt;/code&gt; array, a&lt;br&gt;
&lt;code&gt;hidden&lt;/code&gt; array, indices lining up across them.&lt;/p&gt;

&lt;p&gt;Then: a test case is just a group with &lt;code&gt;n: 1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One case, one name, one hidden boolean. It validates against the model I had already&lt;br&gt;
written without changing a line of it, it invents no schema, and when the suite reaches&lt;br&gt;
the simulator it is a format the simulator already knows how to run. The editor&lt;br&gt;
serialises its table into exactly that on submit.&lt;/p&gt;

&lt;p&gt;I have written the other version of this before — the one with parallel arrays and a&lt;br&gt;
comment explaining how the indices correspond. It works right up until someone deletes a&lt;br&gt;
row from one array and not the others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not reimplementing the simulator
&lt;/h2&gt;

&lt;p&gt;The runner wrapper was the piece I was most nervous about, and the design question was&lt;br&gt;
where the simulation actually happens.&lt;/p&gt;

&lt;p&gt;The tempting answer was Ruby: run the circuit server-side, in the language the rest of&lt;br&gt;
the grading code is written in. I talked myself out of it in about ten minutes. It means&lt;br&gt;
reimplementing thousands of lines of simulation logic, and then maintaining two engines&lt;br&gt;
that will drift — and the failure mode of that drift is a student's circuit passing in&lt;br&gt;
the browser and failing at grading. For a grading tool there is no worse bug.&lt;/p&gt;

&lt;p&gt;Driving a real browser was the next candidate, and &lt;code&gt;capybara-playwright-driver&lt;/code&gt; is&lt;br&gt;
already in the Gemfile, so it looked nearly free. A browser process per submission is not&lt;br&gt;
nearly free.&lt;/p&gt;

&lt;p&gt;Shelling out to Node was the closest call, because there is precedent: the Yosys&lt;br&gt;
integration in this repository does exactly that. I went with an HTTP call to a sidecar&lt;br&gt;
service instead, mirroring the other half of that same integration — a URL from the&lt;br&gt;
environment, the same timeout-and-post shape the simulator controller already uses. It&lt;br&gt;
keeps the engine on the frontend repository's release cadence, where that code and the&lt;br&gt;
people who know it live.&lt;/p&gt;

&lt;p&gt;Every failure collapses into one error class: bad circuit data, a non-2xx, a timeout, a&lt;br&gt;
non-JSON body, an empty result. That is deliberate. The only caller is a background job,&lt;br&gt;
and there is exactly one right answer to all of them — mark the run failed, leave the&lt;br&gt;
student's grade alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the project stands
&lt;/h2&gt;

&lt;p&gt;Five merged, eight open, three written and waiting. The protocol layer is done and&lt;br&gt;
merged. Grade passback, deep linking, and roster sync are all in the queue. Autograding is&lt;br&gt;
started.&lt;/p&gt;

&lt;p&gt;The plan grew by three pull requests this week, which on a burndown chart is a bad week.&lt;br&gt;
It is the most useful thing I did. I have spent eleven weeks getting good at making&lt;br&gt;
mechanisms work, and the evaluation caught me having built most of a system that no&lt;br&gt;
instructor could have used, because I kept reading a feature request as a description of&lt;br&gt;
a machine instead of a description of a person trying to get something done.&lt;/p&gt;

&lt;p&gt;Next week: the editor lands, and then the settings and the grading result behind it.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>opensource</category>
      <category>career</category>
      <category>learning</category>
    </item>
    <item>
      <title>Week 10: The Handshake Begins</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sat, 08 Aug 2026 02:21:38 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-10-the-handshake-begins-3j5n</link>
      <guid>https://dev.to/magic-peach/week-10-the-handshake-begins-3j5n</guid>
      <description>&lt;p&gt;This week the first leg of the LTI 1.3 handshake shipped as&lt;br&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7746" rel="noopener noreferrer"&gt;#7746&lt;/a&gt;, a static analyser and I&lt;br&gt;
disagreed about CSRF, and I spent an afternoon cleaning up a database mess I had made&lt;br&gt;
for myself weeks earlier.&lt;/p&gt;
&lt;h2&gt;
  
  
  OIDC login initiation
&lt;/h2&gt;

&lt;p&gt;The endpoint is &lt;code&gt;/lti/login&lt;/code&gt;, and it is the URL that the tool configuration from&lt;br&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7659" rel="noopener noreferrer"&gt;#7659&lt;/a&gt; has been advertising as&lt;br&gt;
&lt;code&gt;oidc_initiation_url&lt;/code&gt; for three weeks while returning 404.&lt;/p&gt;

&lt;p&gt;An LMS calls it with &lt;code&gt;iss&lt;/code&gt;, &lt;code&gt;login_hint&lt;/code&gt;, and optionally &lt;code&gt;client_id&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;lti_deployment_id&lt;/code&gt;. CircuitVerse resolves those to a registered deployment, mints a&lt;br&gt;
one-time nonce and a signed state, and redirects the browser to the platform's&lt;br&gt;
authorization endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;302 Found
location: http://canvas.docker/api/lti/authorize_redirect?scope=openid
  &amp;amp;response_type=id_token&amp;amp;response_mode=form_post&amp;amp;prompt=none
  &amp;amp;client_id=...&amp;amp;redirect_uri=.../lti/launch&amp;amp;login_hint=demo-student
  &amp;amp;nonce=ddbf28cc...&amp;amp;state=eyJfcmFpbHMi...--e3a4d84925a8df11c8dc6a...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;response_mode=form_post&lt;/code&gt; and &lt;code&gt;prompt=none&lt;/code&gt; are what make the platform POST the token&lt;br&gt;
straight back rather than showing a login screen. The &lt;code&gt;--&lt;/code&gt; in the state is the HMAC&lt;br&gt;
separator — that is the signed-state design from week 6 finally in production form.&lt;/p&gt;

&lt;p&gt;64 lines of controller code, about 150 lines of tests.&lt;/p&gt;
&lt;h2&gt;
  
  
  Arguing with CodeQL
&lt;/h2&gt;

&lt;p&gt;CI came back with CodeQL failing: &lt;em&gt;CSRF protection weakened or disabled.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It was pointing at this line, which I had edited to add the new action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;skip_before_action&lt;/span&gt; &lt;span class="ss"&gt;:verify_authenticity_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="sx"&gt;%i[launch oidc_login]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My first reaction was that this is a false positive. Of course there is no CSRF token —&lt;br&gt;
the request originates from Canvas. There is no way for an LMS to hold a token minted by&lt;br&gt;
our forms.&lt;/p&gt;

&lt;p&gt;My second reaction, which took longer, was that the scanner was pointing at something&lt;br&gt;
real even if its conclusion did not fit. &lt;code&gt;skip_before_action&lt;/code&gt; is a blunt instrument: it&lt;br&gt;
turns the check off for the entire action, for every request shape, forever. What I&lt;br&gt;
actually wanted to say is narrower — &lt;em&gt;this particular kind of request does not need a&lt;br&gt;
token, and everything else still does.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;So instead of skipping, I overrode the predicate the check uses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verified_request?&lt;/span&gt;
  &lt;span class="k"&gt;super&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action_name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"oidc_login"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:iss&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:login_hint&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Token verification stays fully active for every other action and every other request&lt;br&gt;
shape. Only a well-formed LTI initiation is treated as verified. And the justification&lt;br&gt;
holds up: the action reads no session, writes nothing, and the signed state it returns is&lt;br&gt;
what actually protects the launch that follows.&lt;/p&gt;

&lt;p&gt;I checked this was real rather than incidental by removing the override and running the&lt;br&gt;
tests — the cross-site POST spec fails with &lt;code&gt;InvalidAuthenticityToken&lt;/code&gt;. Worth doing,&lt;br&gt;
because a test that passes for the wrong reason is worse than no test. The test&lt;br&gt;
environment disables forgery protection by default, so that spec has to switch it back&lt;br&gt;
on explicitly or it proves nothing at all.&lt;/p&gt;

&lt;p&gt;CodeQL went green, and the code is genuinely narrower than what I started with. The&lt;br&gt;
scanner was more right than I initially gave it credit for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What review caught
&lt;/h2&gt;

&lt;p&gt;Three findings on the pull request, all worth having:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ambiguous deployment matches.&lt;/strong&gt; My lookup ended with &lt;code&gt;scope.order(:id).first&lt;/code&gt;. Since&lt;br&gt;
&lt;code&gt;client_id&lt;/code&gt; is optional in an initiation, a platform with several registrations under&lt;br&gt;
one issuer would silently resolve to whichever row happened to have the lowest ID — and&lt;br&gt;
the launch would be sent to a different registration's client ID and authorization&lt;br&gt;
endpoint. Now an ambiguous match is refused with a 404 rather than guessed at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Non-HTTPS authorization endpoints.&lt;/strong&gt; The &lt;code&gt;LtiDeployment&lt;/code&gt; model only validates that&lt;br&gt;
&lt;code&gt;auth_login_url&lt;/code&gt; is present, not that it is a sane URL. Since the redirect uses&lt;br&gt;
&lt;code&gt;allow_other_host: true&lt;/code&gt;, a malformed or hostile value in that column goes straight to&lt;br&gt;
the browser. A &lt;code&gt;javascript:&lt;/code&gt; URL is now rejected before it can reach &lt;code&gt;redirect_to&lt;/code&gt;, and&lt;br&gt;
TLS is required in production while plain http still works locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Registered query parameters being discarded.&lt;/strong&gt; Some platforms register an&lt;br&gt;
authorization endpoint that already carries query parameters. I was replacing the query&lt;br&gt;
string wholesale. The suggested fix merged instead of replaced — and introduced a subtle&lt;br&gt;
bug of its own by merging symbol keys into a hash with string keys, so a platform's own&lt;br&gt;
&lt;code&gt;scope=&lt;/code&gt; would have survived alongside ours as a duplicate parameter. String keys&lt;br&gt;
throughout fixed it.&lt;/p&gt;

&lt;p&gt;That last one is a good reminder that a suggested patch is a suggestion. Both PRs this&lt;br&gt;
week had a fix applied through the GitHub web UI that did not splice cleanly — one&lt;br&gt;
duplicated the tail of a method, leaving four unreachable statements and a duplicate&lt;br&gt;
&lt;code&gt;rescue&lt;/code&gt; clause that failed lint. Convenient, but it still needs reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  The database mess
&lt;/h2&gt;

&lt;p&gt;Somewhere in this week I tried to run the new endpoint against my development database&lt;br&gt;
and got a &lt;code&gt;NOT NULL&lt;/code&gt; violation on a column that does not exist in the merged schema.&lt;/p&gt;

&lt;p&gt;The cause was archaeology. My abandoned POC branch from week 2 had a migration creating&lt;br&gt;
&lt;code&gt;lti_deployments&lt;/code&gt; with a &lt;code&gt;platform_id&lt;/code&gt; column. I ran it against my dev database in June.&lt;br&gt;
The branch was closed; the migration was never rolled back. The merged version of that&lt;br&gt;
table, from a completely different pull request, has no &lt;code&gt;platform_id&lt;/code&gt; — so my dev&lt;br&gt;
database had a table that matched no branch anyone was working on.&lt;/p&gt;

&lt;p&gt;Tracing it was more interesting than expected. The physical column order gave it away:&lt;br&gt;
&lt;code&gt;platform_public_key&lt;/code&gt; sat &lt;em&gt;after&lt;/em&gt; the timestamps, which meant it had been added by a&lt;br&gt;
later &lt;code&gt;add_column&lt;/code&gt; rather than declared inline — which pinned it to the March POC&lt;br&gt;
migration rather than the June one. Twenty-six migration versions were recorded in my&lt;br&gt;
dev database with no corresponding file on any branch, along with an orphaned&lt;br&gt;
&lt;code&gt;lti_platforms&lt;/code&gt; table whose migration no longer exists anywhere.&lt;/p&gt;

&lt;p&gt;I cleaned it surgically rather than dropping the database: exported the two Canvas&lt;br&gt;
registrations my demo depends on, dropped the drifted columns and tables, cleared the&lt;br&gt;
stale migration stamps, ran the real migration, and re-imported the registrations&lt;br&gt;
against the new schema.&lt;/p&gt;

&lt;p&gt;The lesson is cheap to state and I had to learn it the expensive way: &lt;strong&gt;migrations you&lt;br&gt;
run from a branch that never merges do not clean themselves up.&lt;/strong&gt; A dev database is&lt;br&gt;
long-lived state that accumulates the residue of every experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the project stands
&lt;/h2&gt;

&lt;p&gt;Merged: the 1.1 grade passback fix, and the deployment model. Approved and waiting: the&lt;br&gt;
JWKS and tool configuration endpoints, the JWT validator, and now the OIDC login.&lt;/p&gt;

&lt;p&gt;That is four of the twenty-four, with the whole of phase 1's protocol layer either&lt;br&gt;
merged or in the queue.&lt;/p&gt;

&lt;p&gt;Next is the launch itself — verify the state, validate the token through the validator&lt;br&gt;
from week 9, bind the token's deployment to the one the state was issued for, and sign&lt;br&gt;
the user in on their &lt;code&gt;sub&lt;/code&gt;. It is the piece where all the separate bricks finally do&lt;br&gt;
something a user can see: a Canvas click landing a signed-in user inside CircuitVerse.&lt;/p&gt;

&lt;p&gt;It is also the one I need to be most careful with, and I have a list of open questions&lt;br&gt;
to settle before writing it — what happens when a platform's privacy settings mean it&lt;br&gt;
sends no email address at all, what happens when that email already belongs to a&lt;br&gt;
CircuitVerse account, and where the nonce gets recorded so a state cannot be replayed&lt;br&gt;
inside its five-minute window.&lt;/p&gt;

&lt;p&gt;Ten weeks ago I would have just written it and found out.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>circuitverse</category>
      <category>lti</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Week 9: Verifying the Token</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sat, 08 Aug 2026 02:19:37 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-9-verifying-the-token-4mmg</link>
      <guid>https://dev.to/magic-peach/week-9-verifying-the-token-4mmg</guid>
      <description>&lt;p&gt;Two pull requests merged this week and the most security-critical piece of the whole&lt;br&gt;
project went out for review on its own. This was the week the plan from week 7 started&lt;br&gt;
visibly paying off.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two merges
&lt;/h2&gt;

&lt;p&gt;On 28 July, &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7654" rel="noopener noreferrer"&gt;#7654&lt;/a&gt; merged — the&lt;br&gt;
&lt;code&gt;LtiDeployment&lt;/code&gt; model. CircuitVerse can now store which LMS platforms it trusts: the&lt;br&gt;
issuer, the client ID, the deployment ID, and the platform's endpoint URLs, unique per&lt;br&gt;
issuer + client + deployment because that triple is how LTI 1.3 identifies a deployment.&lt;/p&gt;

&lt;p&gt;Nothing reads the table yet. That is the point of building this way, and it is the thing&lt;br&gt;
that needs explaining in every PR description.&lt;/p&gt;
&lt;h2&gt;
  
  
  The validator
&lt;/h2&gt;

&lt;p&gt;The main work was &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7715" rel="noopener noreferrer"&gt;#7715&lt;/a&gt;:&lt;br&gt;
&lt;code&gt;Lti::JwtValidator&lt;/code&gt;, a class with one public method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Lti&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;JwtValidator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deployment&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give it an &lt;code&gt;id_token&lt;/code&gt;, the deployment it should belong to, and the nonce we issued. It&lt;br&gt;
either returns the verified payload or raises. No controller, no session, no Rails&lt;br&gt;
request object. That isolation was forced by the constraint that it had to be its own&lt;br&gt;
pull request, and it is a much better boundary than I would have drawn otherwise.&lt;/p&gt;
&lt;h3&gt;
  
  
  What it has to refuse
&lt;/h3&gt;

&lt;p&gt;The tests are mostly attacks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A token signed by the wrong key.&lt;/strong&gt; The baseline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A token with &lt;code&gt;alg: none&lt;/code&gt;.&lt;/strong&gt; Early JWT libraries would accept an unsigned token if it&lt;br&gt;
declared its algorithm as &lt;code&gt;none&lt;/code&gt;. Any validator that reads the algorithm out of the&lt;br&gt;
token and trusts it is exploitable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An HS256 token where RS256 is expected.&lt;/strong&gt; This is algorithm confusion, and it is the&lt;br&gt;
subtle one. RS256 is asymmetric: the platform signs with a private key, we verify with&lt;br&gt;
their public key, which is published. HS256 is symmetric: the same secret signs and&lt;br&gt;
verifies. If a validator takes the algorithm from the token header and passes the&lt;br&gt;
platform's public key as the key, an attacker can sign a token using that &lt;em&gt;public&lt;/em&gt; key&lt;br&gt;
as an HMAC secret — and the validator verifies it, because it is doing exactly what it&lt;br&gt;
was told. The public key is public, so anyone can do this.&lt;/p&gt;

&lt;p&gt;The defence is to never read the algorithm from the token. Pass an explicit allow-list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;encoded_token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify_signature!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;algorithm: &lt;/span&gt;&lt;span class="s2"&gt;"RS256"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expired, wrong issuer, wrong audience, missing subject, replayed nonce&lt;/strong&gt;, and the&lt;br&gt;
multi-audience case: when &lt;code&gt;aud&lt;/code&gt; is an array with more than one entry, the &lt;code&gt;azp&lt;/code&gt; claim&lt;br&gt;
must name our client ID.&lt;/p&gt;
&lt;h3&gt;
  
  
  Resolving the platform's key
&lt;/h3&gt;

&lt;p&gt;The other half is fetching the right public key. A platform publishes a JWKS endpoint;&lt;br&gt;
the token header carries a &lt;code&gt;kid&lt;/code&gt; identifying which key signed it.&lt;/p&gt;

&lt;p&gt;Two things made this more interesting than expected:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key rotation.&lt;/strong&gt; Platforms rotate keys. A cached key set will not contain a newly&lt;br&gt;
rotated key. So a &lt;code&gt;kid&lt;/code&gt; that is missing from the cache is treated as a signal that&lt;br&gt;
rotation may have happened, and triggers a refetch rather than a wait for expiry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching.&lt;/strong&gt; Without it, every single launch makes an outbound HTTP request to the&lt;br&gt;
platform before anyone can sign in. If the platform's JWKS endpoint is slow, every login&lt;br&gt;
is slow; if it is down, every login is down. A short-lived cache keyed on the JWKS URL&lt;br&gt;
fixes that, with the rotation behaviour above as the escape hatch.&lt;/p&gt;
&lt;h3&gt;
  
  
  What review found
&lt;/h3&gt;

&lt;p&gt;The bot review flagged two failure modes I had not handled, and both were correct:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A blank &lt;code&gt;jwks_url&lt;/code&gt;.&lt;/strong&gt; A deployment configured with only a stored public key and no&lt;br&gt;
JWKS URL would still make an HTTP request, because a blank URL parses as a relative path&lt;br&gt;
rather than failing. So every launch for that deployment took a pointless network trip&lt;br&gt;
before falling back. One guard clause.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Malformed stored key data.&lt;/strong&gt; &lt;code&gt;OpenSSL::PKey::RSA.new&lt;/code&gt; raises &lt;code&gt;OpenSSL::PKey::RSAError&lt;/code&gt;&lt;br&gt;
on bad PEM data, and I was only rescuing &lt;code&gt;JWT::DecodeError&lt;/code&gt;. So a misconfigured&lt;br&gt;
deployment record would propagate an OpenSSL exception out of a method whose entire&lt;br&gt;
contract is "raises &lt;code&gt;ValidationError&lt;/code&gt;". Callers written against that contract would not&lt;br&gt;
catch it.&lt;/p&gt;

&lt;p&gt;That second one is a good example of a bug that is invisible while you are writing the&lt;br&gt;
code and obvious once named. My mental model was "this method validates tokens, so it&lt;br&gt;
raises validation errors." The actual behaviour was "this method raises validation&lt;br&gt;
errors &lt;em&gt;unless the database row is malformed&lt;/em&gt;, in which case it raises something else&lt;br&gt;
entirely."&lt;/p&gt;

&lt;p&gt;I fixed both in &lt;code&gt;fix(lti): harden JwtValidator key resolution per review&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The refactor request
&lt;/h2&gt;

&lt;p&gt;Late in the week, a maintainer asked something more interesting than a bug report:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;could you suggest refactoring this class using JWT::Token or JWT::EncodedToken&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My implementation called &lt;code&gt;JWT.decode&lt;/code&gt; twice — once with verification disabled to read&lt;br&gt;
the header and pick a key by &lt;code&gt;kid&lt;/code&gt;, then again with verification on. That works, and it&lt;br&gt;
has a smell: there is a window where the code holds an unverified, parsed token, and&lt;br&gt;
nothing but my own discipline stops me from reading a claim out of it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;JWT::EncodedToken&lt;/code&gt; closes that window at the library level. &lt;code&gt;#payload&lt;/code&gt; refuses to&lt;br&gt;
decode until both the signature and the claims have been verified:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;payload&lt;/span&gt;
  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DecodeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@signature_verified&lt;/span&gt;
  &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DecodeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'...'&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@claims_verified&lt;/span&gt;
  &lt;span class="n"&gt;decoded_payload&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The header is still read unverified, which is unavoidable — you cannot pick a key&lt;br&gt;
without knowing which key was used — but it is only ever used to select a candidate key,&lt;br&gt;
and the ordering the class used to depend on by convention is now enforced by the&lt;br&gt;
library.&lt;/p&gt;

&lt;p&gt;That is a strictly better property: the difference between "the code is correct" and&lt;br&gt;
"the code cannot easily be made incorrect by the next person to edit it."&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am taking from the week
&lt;/h2&gt;

&lt;p&gt;The isolation was the win. Because the validator was its own pull request, with no&lt;br&gt;
controller and no session attached, both the review comments were about &lt;em&gt;the validator&lt;/em&gt;&lt;br&gt;
— its error contract, its failure modes — rather than about whether the launch flow&lt;br&gt;
around it made sense.&lt;/p&gt;

&lt;p&gt;Compare that to my POC, where the same class was buried in 2,000 lines and nobody looked&lt;br&gt;
at it at all.&lt;/p&gt;

&lt;p&gt;Next week: the first half of the handshake ships, and a scanner disagrees with me about&lt;br&gt;
CSRF.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>circuitverse</category>
      <category>lti</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Week 8: The First Merge, and Learning to Wait</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sat, 08 Aug 2026 02:18:20 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-8-the-first-merge-and-learning-to-wait-1pa2</link>
      <guid>https://dev.to/magic-peach/week-8-the-first-merge-and-learning-to-wait-1pa2</guid>
      <description>&lt;p&gt;On 26 July, &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7647" rel="noopener noreferrer"&gt;#7647&lt;/a&gt; merged. It&lt;br&gt;
was the first piece of my project to land in CircuitVerse's master branch, fourteen&lt;br&gt;
days after I opened it and about seven weeks into the project.&lt;/p&gt;

&lt;p&gt;It is a small change. It makes sure an LTI 1.1 grade is written to CircuitVerse's&lt;br&gt;
database before it is pushed to the LMS gradebook, so the two cannot silently disagree&lt;br&gt;
when a local save fails. Plus the fix from last week's review — clearing stale grading&lt;br&gt;
context only after a launch has been verified, so a forged request cannot wipe a&lt;br&gt;
teacher's session on its way to being rejected.&lt;/p&gt;

&lt;p&gt;Most of this week was spent not writing code, which is a thing I want to write about&lt;br&gt;
honestly rather than skip over.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually shipped
&lt;/h2&gt;

&lt;p&gt;The merged change has two halves, and the second only exists because of review.&lt;/p&gt;

&lt;p&gt;The first is ordering: persist the grade, then push it to the LMS. Previously the&lt;br&gt;
passback ran first, so a local save that failed validation left the gradebook holding a&lt;br&gt;
score CircuitVerse had no record of. The LMS is the more visible system, which makes&lt;br&gt;
this the worse direction to be wrong in — a student and an instructor both see a grade&lt;br&gt;
that does not exist anywhere in our database.&lt;/p&gt;

&lt;p&gt;The second is the fix from &lt;code&gt;anxkhn&lt;/code&gt;'s review: stale grading context is now cleared only&lt;br&gt;
after a launch has been verified, not before. An unauthenticated request that gets&lt;br&gt;
rejected can no longer wipe a teacher's in-progress grading session on its way out.&lt;/p&gt;

&lt;p&gt;Both are small. Neither is glamorous. Together they mean the gradebook and CircuitVerse&lt;br&gt;
cannot silently disagree, in an integration that schools are using today, which is worth&lt;br&gt;
more than another thousand lines of the 1.3 work sitting unmerged on a branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of the week
&lt;/h2&gt;

&lt;p&gt;My commit graph for 16–25 July is empty. Two pull requests were open and waiting&lt;br&gt;
(&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7654" rel="noopener noreferrer"&gt;#7654&lt;/a&gt; and&lt;br&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7659" rel="noopener noreferrer"&gt;#7659&lt;/a&gt;), one was awaiting the&lt;br&gt;
final look before merge, and the next thing in the plan — the JWT validator — depended&lt;br&gt;
on decisions inside them.&lt;/p&gt;

&lt;p&gt;Early in the project, a week like this would have gone one of two ways. Either I would&lt;br&gt;
have sat refreshing the PR page, or I would have started building three PRs ahead and&lt;br&gt;
piled up work that might need reworking depending on how the review went.&lt;/p&gt;

&lt;p&gt;I had done the second thing already, in weeks 2 and 3, and it is how I ended up with a&lt;br&gt;
2,000 line branch nobody could review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What review latency actually is
&lt;/h2&gt;

&lt;p&gt;The reframe that helped: maintainer response time is not an obstacle in the process. It&lt;br&gt;
&lt;em&gt;is&lt;/em&gt; the process.&lt;/p&gt;

&lt;p&gt;CircuitVerse's maintainers review contributions alongside their own jobs. A day or two&lt;br&gt;
of latency on a pull request is not neglect, it is the normal operating speed of a&lt;br&gt;
volunteer-maintained project. Every plan I make has to be built for that speed rather&lt;br&gt;
than in spite of it.&lt;/p&gt;

&lt;p&gt;Concretely, that changes what "being blocked" means. If PR N is in review and PR N+1&lt;br&gt;
depends on it, I am not blocked from working — I am blocked from &lt;em&gt;committing to a&lt;br&gt;
particular implementation&lt;/em&gt; of N+1. Those are different, and the gap between them is&lt;br&gt;
where the useful work of a waiting week lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did with it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Read the review comments properly.&lt;/strong&gt; Not just applying them, but working out the&lt;br&gt;
general rule behind each. &lt;code&gt;anxkhn&lt;/code&gt;'s note about clearing grading context before&lt;br&gt;
verification was specifically about one method, but the rule it implies — nothing that&lt;br&gt;
mutates state may happen before the security check, including deletes — is one I could&lt;br&gt;
then apply to code they had not looked at. A review comment is a sample from a&lt;br&gt;
distribution of mistakes I am prone to. Fixing the sample is the minimum; finding the&lt;br&gt;
distribution is the value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrote the next PR's tests before its code.&lt;/strong&gt; The JWT validator was next, and while I&lt;br&gt;
did not want to commit to its implementation before the deployment model merged, the&lt;br&gt;
&lt;em&gt;attacks it must survive&lt;/em&gt; did not depend on any of that. A token signed with the wrong&lt;br&gt;
key. A token with &lt;code&gt;alg: none&lt;/code&gt;. An HS256 token where the validator expects RS256 —&lt;br&gt;
algorithm confusion, where an attacker signs with the public key as an HMAC secret and a&lt;br&gt;
naive validator accepts it. Expired tokens, wrong issuer, wrong audience, missing&lt;br&gt;
subject, replayed nonce.&lt;/p&gt;

&lt;p&gt;Writing those first meant that when the validator got written, its shape was determined&lt;br&gt;
by what it had to refuse rather than by what I found convenient to implement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Read other people's LTI implementations.&lt;/strong&gt; Specifically for how they handle key&lt;br&gt;
rotation — a platform can publish a new signing key at any time, and a tool that caches&lt;br&gt;
the key set has to notice. The pattern I settled on: cache the key set, but if a token&lt;br&gt;
arrives with a &lt;code&gt;kid&lt;/code&gt; that is not in the cache, refetch immediately rather than waiting&lt;br&gt;
for the cache to expire. Rotation is exactly the case where the cache is stale, so&lt;br&gt;
treating a cache miss as a rotation signal is the right default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kept the queue moving.&lt;/strong&gt; The point of the twenty-four PR plan is a steady stream of&lt;br&gt;
small reviewable things. That only works if the queue is never empty from my side, and&lt;br&gt;
never so full that it becomes a burden. Two open at a time, both small, both green,&lt;br&gt;
turned out to be about right.&lt;/p&gt;

&lt;h2&gt;
  
  
  On the size of the win
&lt;/h2&gt;

&lt;p&gt;Fourteen days from open to merge, for a change of about 150 lines that fixes a real bug&lt;br&gt;
in a feature real users have today.&lt;/p&gt;

&lt;p&gt;Measured in lines of code per day it is not much. But the previous seven weeks produced&lt;br&gt;
zero merged lines, and the thing that changed was not that I started working harder — it&lt;br&gt;
was that I started sending work in a form somebody could say yes to.&lt;/p&gt;

&lt;p&gt;That is the actual lesson of the first half of this project, and I could not have&lt;br&gt;
learned it by reading about it. I had to spend three weeks building something good that&lt;br&gt;
could not merge.&lt;/p&gt;

&lt;p&gt;There is a version of this project where I never write the POC, go straight to small&lt;br&gt;
PRs, and have six things merged by now. I do not think I would have understood &lt;em&gt;why&lt;/em&gt;&lt;br&gt;
they had to be small. The POC being closed is what made the constraint make sense rather&lt;br&gt;
than feel like bureaucracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where things stand
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Merged:&lt;/strong&gt; the 1.1 grade passback fix.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approved and waiting:&lt;/strong&gt; the deployment model, and the JWKS and tool configuration
endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next:&lt;/strong&gt; the JWT validator — the security-critical brick, reviewed on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Next week: the token validator, and finding out my key-resolution code had two failure&lt;br&gt;
modes I had not handled.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>circuitverse</category>
      <category>lti</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Week 7: Creating phases for the tasks</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sat, 08 Aug 2026 02:16:10 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-7-twenty-four-pull-requests-46ff</link>
      <guid>https://dev.to/magic-peach/week-7-twenty-four-pull-requests-46ff</guid>
      <description>&lt;p&gt;This was the week the project got a plan it could actually execute, and the week a&lt;br&gt;
reviewer found a security bug in a fix I had already convinced myself was correct.&lt;/p&gt;

&lt;h2&gt;
  
  
  The review
&lt;/h2&gt;

&lt;p&gt;I had opened &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7647" rel="noopener noreferrer"&gt;#7647&lt;/a&gt; at the end&lt;br&gt;
of last week — the small fix ensuring an LTI 1.1 grade is saved locally before being&lt;br&gt;
pushed to the LMS.&lt;/p&gt;

&lt;p&gt;On 14 July, &lt;code&gt;anxkhn&lt;/code&gt; reviewed it. The first comment was housekeeping: rebase and look&lt;br&gt;
at failing CI. I rebased, tests went green, and I said so.&lt;/p&gt;

&lt;p&gt;The second comment is the one worth writing about:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;storing the context is fixed, but it is still cleared before &lt;code&gt;valid_request?&lt;/code&gt;. for&lt;br&gt;
example, a fake launch could clear a teacher's valid grading session and prevent the&lt;br&gt;
next grade passback. please clear it only after a valid launch too.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let me unpack what that means, because it is genuinely subtle.&lt;/p&gt;

&lt;p&gt;An LTI 1.1 launch stores some grading context in the session — the outcome service URL&lt;br&gt;
the LMS wants scores posted to, and which assignment is being graded. My change made&lt;br&gt;
sure that context is written &lt;em&gt;after&lt;/em&gt; the launch signature is verified. Good.&lt;/p&gt;

&lt;p&gt;But the &lt;em&gt;clearing&lt;/em&gt; of stale context happened in a &lt;code&gt;before_action&lt;/code&gt;, which runs before&lt;br&gt;
&lt;code&gt;valid_request?&lt;/code&gt; — before we know whether the launch is genuine at all.&lt;/p&gt;

&lt;p&gt;So: a teacher launches from Canvas, gets a valid grading session, starts grading.&lt;br&gt;
Meanwhile anyone can POST garbage at &lt;code&gt;/lti/launch&lt;/code&gt; with no valid signature. That request&lt;br&gt;
gets rejected — 401, correct — but on its way to being rejected it has already wiped&lt;br&gt;
the teacher's grading context. Their next grade passback silently fails.&lt;/p&gt;

&lt;p&gt;An unauthenticated request cannot sign in, cannot read anything, cannot write&lt;br&gt;
anything — and can still degrade a legitimate user's session as a side effect of being&lt;br&gt;
rejected.&lt;/p&gt;

&lt;p&gt;I had read that code many times. I had written the fix for the adjacent bug in the same&lt;br&gt;
method. I did not see it, because I was asking "is the write ordered correctly?" and the&lt;br&gt;
bug was in the delete.&lt;/p&gt;

&lt;p&gt;What I take from it: &lt;strong&gt;when you fix ordering around a security check, audit every&lt;br&gt;
operation on that path, not just the one you came for.&lt;/strong&gt; The invariant is not "the write&lt;br&gt;
happens after verification." It is "nothing that affects state happens before&lt;br&gt;
verification." I had fixed one instance of a general rule while leaving another in&lt;br&gt;
place.&lt;/p&gt;

&lt;p&gt;I also want to record that this is the value of review that no amount of care&lt;br&gt;
substitutes for. Not because the reviewer knew something I did not, but because they&lt;br&gt;
came to the code without my assumption about which line was interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other review comment
&lt;/h2&gt;

&lt;p&gt;The same day, &lt;code&gt;anxkhn&lt;/code&gt; reviewed&lt;br&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7654" rel="noopener noreferrer"&gt;#7654&lt;/a&gt;, the &lt;code&gt;LtiDeployment&lt;/code&gt;&lt;br&gt;
model, and approved it — with a nitpick:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;could you change &lt;code&gt;Fixes #7405&lt;/code&gt; to &lt;code&gt;Part of #7405&lt;/code&gt;? this PR is the first foundation&lt;br&gt;
step, so merging it should not close the larger issue.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Small, and it corrected a real misunderstanding. I had been writing &lt;code&gt;Fixes #7405&lt;/code&gt; on&lt;br&gt;
every LTI pull request out of habit. GitHub's auto-close keywords mean the first one to&lt;br&gt;
merge would have closed the umbrella issue tracking the entire LTI 1.3 upgrade — with&lt;br&gt;
about 90% of it unbuilt.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Part of #7405&lt;/code&gt; links without closing. I have used that phrasing on every LTI PR since.&lt;/p&gt;

&lt;h2&gt;
  
  
  Twenty-four pull requests
&lt;/h2&gt;

&lt;p&gt;The bigger piece of the week was planning. On 14 July I wrote out the remaining work as&lt;br&gt;
a sequence of pull requests, and the constraint I set was that no single one should&lt;br&gt;
exceed roughly 200 hand-written lines. Generated files like &lt;code&gt;Gemfile.lock&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;db/schema.rb&lt;/code&gt; do not count, but get flagged for reviewers.&lt;/p&gt;

&lt;p&gt;It came to twenty-four.&lt;/p&gt;

&lt;p&gt;The number was startling and also clarifying. What used to be "implement LTI 1.3" became&lt;br&gt;
a dependency-ordered queue, with phases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phase 0&lt;/strong&gt; — the credibility work: fix the 1.1 bug that already affects real users.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 1&lt;/strong&gt; — the launch core, five PRs: gems and the deployment model; key manager
and public endpoints; the JWT validator alone; OIDC login initiation; the launch
itself; then the resource link model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 2&lt;/strong&gt; — Assignment and Grade Services, so grades flow to the LMS gradebook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 3&lt;/strong&gt; — Deep Linking, so an instructor can embed specific CircuitVerse content.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 4&lt;/strong&gt; — Names and Roles, so the roster syncs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 5&lt;/strong&gt; — autograding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The thing that surprised me is how much the constraint improved the &lt;em&gt;design&lt;/em&gt;, not just&lt;br&gt;
the delivery. Forced to make &lt;code&gt;Lti::JwtValidator&lt;/code&gt; a standalone pull request, it has to be&lt;br&gt;
a class that takes a token and a deployment and returns a payload — no controller, no&lt;br&gt;
session, no Rails request. That is a better boundary than I would have drawn if I were&lt;br&gt;
free to let it reach into whatever it wanted.&lt;/p&gt;

&lt;p&gt;The same happened to the OIDC login. Made to stand alone, it has to hand back a state&lt;br&gt;
that the launch can verify later with no shared memory between them — which is exactly&lt;br&gt;
the signed-state design from last week. The constraint and the correct architecture&lt;br&gt;
pointed the same way.&lt;/p&gt;

&lt;p&gt;Two rules I wrote down alongside the plan:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If a PR crosses ~200 lines, split the spec edge cases into a follow-up — &lt;strong&gt;never&lt;/strong&gt;
thin out security validation to fit the budget.&lt;/li&gt;
&lt;li&gt;Every PR description names which future PR depends on it, so a maintainer can see why
a change with no user-visible behaviour deserves to merge.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That second rule is doing a lot of work. The hardest thing about this approach is that&lt;br&gt;
most of the PRs do nothing on their own. A deployments table nothing reads. A validator&lt;br&gt;
nothing calls. Without the dependency note, each looks like dead code, and "we don't&lt;br&gt;
merge unused code" is a reasonable instinct for a maintainer to have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shipping against the plan
&lt;/h2&gt;

&lt;p&gt;Two PRs went out under the new scheme:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7654" rel="noopener noreferrer"&gt;#7654&lt;/a&gt; — the &lt;code&gt;LtiDeployment&lt;/code&gt;&lt;br&gt;
model: issuer, client ID, deployment ID, and the platform's endpoint URLs, unique per&lt;br&gt;
issuer + client + deployment. 97 lines. Storage only, nothing reads it yet. Approved&lt;br&gt;
the day it was reviewed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7659" rel="noopener noreferrer"&gt;#7659&lt;/a&gt; — the JWKS and tool&lt;br&gt;
configuration endpoints, plus a key manager that owns CircuitVerse's own RSA keypair.&lt;br&gt;
This is what makes CircuitVerse &lt;em&gt;registerable&lt;/em&gt;: an LMS admin fetches the tool&lt;br&gt;
configuration, and the platform can fetch our public key to verify anything we sign.&lt;br&gt;
193 lines, behind the flag.&lt;/p&gt;

&lt;p&gt;Both were reviewable in a sitting. Which, compared to a 2,000 line POC closed the next&lt;br&gt;
day, is the whole difference.&lt;/p&gt;

&lt;p&gt;Next week: waiting, and what I did with it.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>circuitverse</category>
      <category>opensource</category>
      <category>planning</category>
    </item>
    <item>
      <title>Week 6: Signing the State, and Closing My Own Pull Requests</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sat, 08 Aug 2026 02:13:42 +0000</pubDate>
      <link>https://dev.to/magic-peach/week-6-signing-the-state-and-closing-my-own-pull-requests-4nkb</link>
      <guid>https://dev.to/magic-peach/week-6-signing-the-state-and-closing-my-own-pull-requests-4nkb</guid>
      <description>&lt;p&gt;Three things happened this week: I undid the cookie change from week 5 with something&lt;br&gt;
better, I put the entire LTI 1.3 feature behind a flag that defaults to off, and I&lt;br&gt;
closed two of my own pull requests.&lt;/p&gt;
&lt;h2&gt;
  
  
  Undoing the cookie
&lt;/h2&gt;

&lt;p&gt;Last week I set &lt;code&gt;SameSite=None&lt;/code&gt; on the session cookie so it would survive the&lt;br&gt;
cross-site POST that carries the &lt;code&gt;id_token&lt;/code&gt; back from the platform. It worked, and it&lt;br&gt;
weakened a CSRF defence across the whole application to serve one endpoint.&lt;/p&gt;

&lt;p&gt;This week's commit is&lt;br&gt;
&lt;code&gt;fix(lti): sign the OIDC state instead of weakening the session cookie&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The reframe that unlocked it: I had been treating "the state must survive the round&lt;br&gt;
trip" as "the state must be &lt;em&gt;stored&lt;/em&gt; somewhere that survives the round trip." Those are&lt;br&gt;
not the same requirement. The state does not need to be stored at all. It needs to be&lt;br&gt;
&lt;em&gt;recognisable&lt;/em&gt; when it comes back.&lt;/p&gt;

&lt;p&gt;So instead of keeping a value in the session and comparing, the state becomes a signed,&lt;br&gt;
self-contained token carrying its own payload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;LTI_STATE_PURPOSE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"lti.launch.state"&lt;/span&gt;
&lt;span class="no"&gt;LTI_STATE_TTL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;

&lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lti_state_verifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s2"&gt;"nonce"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;nonce&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"deployment_id"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="ss"&gt;purpose: &lt;/span&gt;&lt;span class="no"&gt;LTI_STATE_PURPOSE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;expires_in: &lt;/span&gt;&lt;span class="no"&gt;LTI_STATE_TTL&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Rails.application.message_verifier&lt;/code&gt; signs it with the application's secret. When the&lt;br&gt;
launch returns, we verify the signature, and if it checks out we know the contents came&lt;br&gt;
from us and have not been altered. The nonce and the deployment ID ride inside the&lt;br&gt;
token itself.&lt;/p&gt;

&lt;p&gt;What this buys:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No session, so no cookie problem.&lt;/strong&gt; Nothing needs to survive the cross-site POST
except the parameter the platform is already sending back to us.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cookie policy change.&lt;/strong&gt; &lt;code&gt;SameSite=Lax&lt;/code&gt; stays as it is for every other page on
the site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No server-side state.&lt;/strong&gt; No table, no cleanup job, no expiry sweeper.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The signature is the CSRF defence.&lt;/strong&gt; Only CircuitVerse can mint a valid state, so a
launch carrying one is answering an initiation we started.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It expires.&lt;/strong&gt; Five minutes, enforced by the verifier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;purpose&lt;/code&gt; argument is a detail worth calling out. It scopes the signature to this&lt;br&gt;
one use, so a token signed elsewhere in the application with the same secret will not&lt;br&gt;
verify here. Same secret, different purposes, no cross-use.&lt;/p&gt;

&lt;p&gt;The general lesson: when a constraint blocks you, check whether you are solving the&lt;br&gt;
constraint or the requirement behind it. "Survive a cross-site POST" sounded like a&lt;br&gt;
cookie problem. It was a state-management problem, and moving the state out of the&lt;br&gt;
session dissolved the cookie question entirely.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flag
&lt;/h2&gt;

&lt;p&gt;The commit &lt;code&gt;feat(lti): gate LTI 1.3 behind a default-off lti_advantage flag&lt;/code&gt; does what&lt;br&gt;
it says: every 1.3 endpoint returns 404 unless an operator turns the flag on.&lt;/p&gt;

&lt;p&gt;The reason is specific. The 1.3 launch &lt;strong&gt;auto-provisions user accounts&lt;/strong&gt;. A launch from&lt;br&gt;
a registered platform creates a CircuitVerse user, confirmed, ready to sign in. That is&lt;br&gt;
correct behaviour for the feature and it is also a significant thing to have live in&lt;br&gt;
production the moment a branch merges.&lt;/p&gt;

&lt;p&gt;A default-off flag means the code can land, be reviewed, be tested, and sit there inert&lt;br&gt;
until somebody deliberately switches it on. It decouples "this code is merged" from&lt;br&gt;
"this code is running", which for an auth path is exactly the separation you want.&lt;/p&gt;

&lt;p&gt;There was also a small commit removing some stale comments about CSRF token handling —&lt;br&gt;
notes I had written to myself that had stopped being true after the state change.&lt;br&gt;
Comments that describe an older version of the code are worse than no comments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing two pull requests
&lt;/h2&gt;

&lt;p&gt;On 12 July I closed &lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7510" rel="noopener noreferrer"&gt;#7510&lt;/a&gt;,&lt;br&gt;
the &lt;code&gt;lti-advantage&lt;/code&gt; gem addition, and&lt;br&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7632" rel="noopener noreferrer"&gt;#7632&lt;/a&gt;, the subgroups feature.&lt;/p&gt;

&lt;p&gt;The gem one was straightforward: I had already reversed that decision in week 4 and&lt;br&gt;
gone direct to &lt;code&gt;ruby-jwt&lt;/code&gt;. Leaving the PR open was just noise in the queue. Closing your&lt;br&gt;
own obsolete pull requests is a small courtesy — every open PR is something a maintainer&lt;br&gt;
has to look at at least once to decide it is not for them.&lt;/p&gt;

&lt;p&gt;Subgroups was harder, because that one was finished. Data layer, UI, controller, API&lt;br&gt;
endpoints, scoping, tests. Earlier in the week I had added the REST API v1 endpoints and&lt;br&gt;
fixed a bug where a synced user who was also a subgroup mentor got a duplicate&lt;br&gt;
membership row inserted.&lt;/p&gt;

&lt;p&gt;But it had the same problem as my original POC: it was a large change to a core model,&lt;br&gt;
arriving as one piece, at a moment when the maintainers' attention on my work was&lt;br&gt;
pointed at LTI. It was not going to get the review it needed, and holding it open would&lt;br&gt;
not change that.&lt;/p&gt;

&lt;p&gt;The work is not lost — the branch is intact and the design still holds up. It just is&lt;br&gt;
not the thing I should be spending review capital on right now.&lt;/p&gt;

&lt;p&gt;That framing — &lt;strong&gt;review capacity as a budget you spend&lt;/strong&gt; — was new to me this week.&lt;br&gt;
Maintainer attention is the scarcest resource on the project. Every PR I open spends&lt;br&gt;
some, and if I spend it on something that is not on the critical path, the thing that&lt;br&gt;
is on the critical path waits longer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Opening the first small one
&lt;/h2&gt;

&lt;p&gt;Right at the end of the week I opened&lt;br&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/pull/7647" rel="noopener noreferrer"&gt;#7647&lt;/a&gt;, and it is deliberately&lt;br&gt;
tiny.&lt;/p&gt;

&lt;p&gt;It fixes a bug in the &lt;em&gt;existing&lt;/em&gt; LTI 1.1 code: when a teacher grades a project in an LTI&lt;br&gt;
session, the score was pushed to the LMS before the grade was saved locally. If the&lt;br&gt;
local save then failed validation, the LMS would show a grade that CircuitVerse had&lt;br&gt;
never stored. The gradebook and the source of truth silently disagree.&lt;/p&gt;

&lt;p&gt;It has nothing to do with 1.3. I picked it as the first thing to send precisely because&lt;br&gt;
it is small, self-contained, and improves the integration that already has real users —&lt;br&gt;
and because it demonstrates I can send something a maintainer can review in ten minutes.&lt;/p&gt;

&lt;p&gt;Which, after week 3, felt like the point.&lt;/p&gt;

&lt;p&gt;Next week: a reviewer finds a bug in it that I had looked straight past, and I cut the&lt;br&gt;
remaining work into twenty-four pull requests.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>lti</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Community Bonding: Before the First Line of Project Code</title>
      <dc:creator>Akanksha Trehun</dc:creator>
      <pubDate>Sat, 08 Aug 2026 02:01:18 +0000</pubDate>
      <link>https://dev.to/magic-peach/community-bonding-before-the-first-line-of-project-code-5ia</link>
      <guid>https://dev.to/magic-peach/community-bonding-before-the-first-line-of-project-code-5ia</guid>
      <description>&lt;p&gt;Coding starts on 1 June. This post is about the three weeks before that — what I did&lt;br&gt;
with them, and what I thought the project was going to be, which turned out to be only&lt;br&gt;
partly right.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I got here
&lt;/h2&gt;

&lt;p&gt;By the time the accepted projects were announced, I had already sent CircuitVerse about&lt;br&gt;
forty pull requests.&lt;/p&gt;

&lt;p&gt;That number sounds more impressive than it is. A lot of them were typo fixes in&lt;br&gt;
documentation and specs — genuinely trivial changes, the kind you send while you are&lt;br&gt;
still working out where anything lives. Some were closed. Some are still open.&lt;/p&gt;

&lt;p&gt;But they did their job, which was less about the changes themselves than about learning&lt;br&gt;
the shape of the project: how the app is laid out, what the review process feels like,&lt;br&gt;
which parts of the codebase are load-bearing and which are quiet corners nobody has&lt;br&gt;
touched in three years.&lt;/p&gt;

&lt;p&gt;The ones I am actually pleased with came later and were small bug fixes rather than&lt;br&gt;
typos — scoping an assignments index to the current user's groups, removing a duplicate&lt;br&gt;
&lt;code&gt;update!&lt;/code&gt; call, adding &lt;code&gt;includes(:user)&lt;/code&gt; to some notification callbacks that were firing&lt;br&gt;
N+1 queries.&lt;/p&gt;

&lt;p&gt;One of them turned out to be a signpost: a partial unique index on&lt;br&gt;
&lt;code&gt;assignments.lti_consumer_key&lt;/code&gt;, because LTI 1.1 launches look assignments up by that&lt;br&gt;
column and there was nothing enforcing uniqueness. That was the first time I read&lt;br&gt;
CircuitVerse's LMS integration properly, months before I proposed to rewrite it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning a proposal into issues
&lt;/h2&gt;

&lt;p&gt;The most useful thing I did during bonding was on 25 May, and it produced no code at&lt;br&gt;
all. I broke the project down into four separate feature issues and filed them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/issues/7405" rel="noopener noreferrer"&gt;#7405&lt;/a&gt; — upgrade the LMS
integration from LTI 1.1 to LTI 1.3&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/issues/7406" rel="noopener noreferrer"&gt;#7406&lt;/a&gt; — roster sync, so a
class list flows from the LMS into a CircuitVerse group&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/issues/7407" rel="noopener noreferrer"&gt;#7407&lt;/a&gt; — grade passback via
Assignment and Grade Services&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/CircuitVerse/CircuitVerse/issues/7408" rel="noopener noreferrer"&gt;#7408&lt;/a&gt; — deep linking, so an
instructor can embed specific CircuitVerse content in a course page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A proposal is a document you write for a small audience during a selection process, and&lt;br&gt;
then it mostly sits still. Issues are different. They live in the repository where the&lt;br&gt;
work happens, anyone can comment on them, and they force you to state what you are&lt;br&gt;
actually going to build in terms a maintainer can disagree with &lt;em&gt;before&lt;/em&gt; you have built&lt;br&gt;
anything.&lt;/p&gt;

&lt;p&gt;Writing them also clarified something for me. In the proposal, "LTI 1.3 support" was one&lt;br&gt;
line item. Split into four issues, the dependency structure became obvious: the launch&lt;br&gt;
has to exist before grades can flow back, because grade passback needs the deployment&lt;br&gt;
and resource-link context that a launch establishes. Roster sync and deep linking both&lt;br&gt;
need the same. #7405 is not one of four features, it is the foundation the other three&lt;br&gt;
stand on.&lt;/p&gt;

&lt;p&gt;That ordering shaped everything since.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the code I was proposing to replace
&lt;/h2&gt;

&lt;p&gt;The other substantial piece of bonding was reading CircuitVerse's existing LTI 1.1&lt;br&gt;
integration line by line — the controller, the score submission service, the launch&lt;br&gt;
views, the specs.&lt;/p&gt;

&lt;p&gt;I went in expecting to find it neglected, because 1.1 is the old standard and my project&lt;br&gt;
is the new one. That is not what I found. It is compact and it works, and it is used by&lt;br&gt;
real courses today.&lt;/p&gt;

&lt;p&gt;I did find things I noted for later. Grades were being pushed to the LMS before being&lt;br&gt;
saved locally, so a failed local save could leave the gradebook holding a score&lt;br&gt;
CircuitVerse never stored. A &lt;code&gt;NameError&lt;/code&gt; in a code path that only triggers under&lt;br&gt;
specific conditions. Small gaps, mostly in error handling rather than in the happy path.&lt;/p&gt;

&lt;p&gt;Two of them became my first pull requests of the coding period.&lt;/p&gt;

&lt;p&gt;Reading it also settled a design question I had been vague about in the proposal.&lt;br&gt;
Nothing about my project requires removing 1.1, and plenty of institutions are still on&lt;br&gt;
it. So the plan became strictly additive: 1.3 arrives alongside 1.1, both work, and&lt;br&gt;
nobody's existing setup breaks. "Backward compatible" stopped being a nice phrase in a&lt;br&gt;
proposal and became a constraint I could check every change against.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mergathon
&lt;/h2&gt;

&lt;p&gt;On 26 May, CircuitVerse ran a Mergathon — a coordinated push across all the org's&lt;br&gt;
repositories to deal with the pull request backlog. I spent the day on triage.&lt;/p&gt;

&lt;p&gt;I have permissions to label, close and comment, not to merge, so the work was: read&lt;br&gt;
every open PR, work out whether it was still alive, and close the ones that were not&lt;br&gt;
with an explanation.&lt;/p&gt;

&lt;p&gt;The numbers were bracing. On the Interactive-Book repository I closed around forty-six&lt;br&gt;
stale pull requests, some open since 2019. Thirteen more on the Vue frontend, five on&lt;br&gt;
the blog. On the mobile app there were ten Dependabot PRs all failing for the same&lt;br&gt;
reason — a dependency requiring a newer Flutter SDK than CI was running — so I traced&lt;br&gt;
the root cause once and commented it on all ten. A handful of others were genuinely&lt;br&gt;
green and just needed someone to say so, which I labelled for a maintainer to merge.&lt;/p&gt;

&lt;p&gt;Reading dozens of dead pull requests in one sitting is an unusual way to spend a day and&lt;br&gt;
I learned more from it than from anything else in bonding. Almost none of them died&lt;br&gt;
because the code was wrong. They died from going quiet — the author moved on, the branch&lt;br&gt;
drifted behind master, a maintainer asked a question that was never answered, and then&lt;br&gt;
too much time passed for anyone to want to pick it up.&lt;/p&gt;

&lt;p&gt;The pattern I noticed and did not fully absorb: the bigger a pull request was, the more&lt;br&gt;
likely it was to be in that pile. Small ones either merged or got closed quickly. Large&lt;br&gt;
ones lingered until they were unmergeable.&lt;/p&gt;

&lt;p&gt;I understood that as an observation about &lt;em&gt;other people's&lt;/em&gt; pull requests. It took until&lt;br&gt;
week 3 of the coding period, and having my own large one closed, to understand it was&lt;br&gt;
about mine too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up something to test against
&lt;/h2&gt;

&lt;p&gt;The last piece was practical: getting an LMS I could actually launch from. Reading the&lt;br&gt;
LTI specification tells you what the protocol says. Watching a real Canvas instance&lt;br&gt;
refuse your launch for reasons the spec does not mention teaches you a different set of&lt;br&gt;
things.&lt;/p&gt;

&lt;p&gt;This took longer than expected, which I gather is normal. LTI is a protocol where two&lt;br&gt;
servers have to agree about URLs, keys and identifiers before anything works at all, and&lt;br&gt;
every mismatch surfaces as the same unhelpful failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I expected to be hard
&lt;/h2&gt;

&lt;p&gt;Going into 1 June, I thought the difficulty of this project was the protocol —&lt;br&gt;
signatures, key rotation, the security model, getting the cryptography right.&lt;/p&gt;

&lt;p&gt;Some of that has been hard. But the protocol is a written specification with a right&lt;br&gt;
answer; if you read it carefully you converge on it.&lt;/p&gt;

&lt;p&gt;The part I did not budget for is the one this whole first stretch has actually turned&lt;br&gt;
on: getting the work into a shape somebody else can be confident about. That is not in&lt;br&gt;
any specification, and it is not something I understood until I had done it wrong.&lt;/p&gt;

&lt;p&gt;Next: week 1, and actually writing some code.&lt;/p&gt;

</description>
      <category>gsoc</category>
      <category>programming</category>
      <category>lti</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
