<?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: Mike</title>
    <description>The latest articles on DEV Community by Mike (@yowaye).</description>
    <link>https://dev.to/yowaye</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%2F4161156%2Fa868a92a-7003-4446-a56a-ada80bc114c0.png</url>
      <title>DEV Community: Mike</title>
      <link>https://dev.to/yowaye</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yowaye"/>
    <language>en</language>
    <item>
      <title>I tested my CRS calculator against the Canadian government's own code on 5,000 random profiles</title>
      <dc:creator>Mike</dc:creator>
      <pubDate>Sun, 04 Oct 2026 07:57:59 +0000</pubDate>
      <link>https://dev.to/yowaye/i-tested-my-crs-calculator-against-the-canadian-governments-own-code-on-5000-random-profiles-3b47</link>
      <guid>https://dev.to/yowaye/i-tested-my-crs-calculator-against-the-canadian-governments-own-code-on-5000-random-profiles-3b47</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3llsgodgfdakbf633gao.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3llsgodgfdakbf633gao.png" alt=" " width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Canada picks skilled immigrants through &lt;strong&gt;Express Entry&lt;/strong&gt;, and every candidate gets a &lt;strong&gt;CRS score&lt;/strong&gt; (Comprehensive Ranking System, out of 1,200). The score decides who gets invited, so people check it obsessively — and dozens of websites offer "CRS calculators".&lt;/p&gt;

&lt;p&gt;When I looked at the popular ones, many were simply wrong. In March 2025 the government (IRCC) removed the points for job offers, which used to be worth 50 or 200 points. A year and a half later, several well-ranked calculators still asked about job offers, and one still claimed a job offer was worth 600 points.&lt;/p&gt;

&lt;p&gt;So I built my own, and made one rule: &lt;strong&gt;it has to agree with the official calculator, and I have to be able to prove it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The scoring rules are simple. The edge cases aren't.
&lt;/h2&gt;

&lt;p&gt;On paper, CRS is a set of lookup tables: age, education, language level per ability, Canadian work experience, a spouse's factors, "skill transferability" combinations, and additional points. Each table has its own caps, and some depend on others. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Language points differ depending on whether your spouse is coming with you.&lt;/li&gt;
&lt;li&gt;Second-language points are capped at 22 with a spouse, or 24 without one.&lt;/li&gt;
&lt;li&gt;"Skill transferability" adds up four combinations, but each pair is capped at 50 and the whole section at 100.&lt;/li&gt;
&lt;li&gt;French bonus points depend on your &lt;em&gt;English&lt;/em&gt; level too (50 or 25).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Hand-written test cases catch the obvious bugs. They don't catch the combination you didn't think of.&lt;/p&gt;

&lt;h2&gt;
  
  
  The oracle: run the government's own code
&lt;/h2&gt;

&lt;p&gt;IRCC's official calculator is a page with a big jQuery script. The scoring logic lives in that script, so I downloaded it and ran it in Node's &lt;code&gt;vm&lt;/code&gt; module with a tiny fake jQuery and DOM, just enough for the script to read form answers and write its result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;   &lt;span class="c1"&gt;// fake &amp;lt;select&amp;gt; values&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;  &lt;span class="c1"&gt;// where the script writes results&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;unknown&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="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;
  &lt;span class="k"&gt;for &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;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;append&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;empty&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;trigger&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;prop&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;show&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;hide&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;html&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;on&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;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;m&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;arg&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&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;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;arg&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;/^#&lt;/span&gt;&lt;span class="se"&gt;([\w&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt; option:selected$/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&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;sel&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&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;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arg&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#crs-form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;api&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&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;submit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fn&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;api&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;return&lt;/span&gt; &lt;span class="nx"&gt;api&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;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fakeElement&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;window&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="nx"&gt;vm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;runInContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;irccScript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// fill `sel` from a profile, call the captured submit handler, parse the total out of `html`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With that in place, the test generates &lt;strong&gt;5,000 random profiles&lt;/strong&gt;: every age, education level, test type (IELTS, CELPIP, PTE Core, TEF, TCF), language level, work history, spouse combination and bonus. It then asserts that my engine and the government's script give the same total.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the oracle found
&lt;/h2&gt;

&lt;p&gt;The first runs failed, and every failure taught me something. Mostly I had misread an interaction between two rules, such as how the spouse tables change the caps.&lt;/p&gt;

&lt;p&gt;Two failures were different. After checking them against IRCC's &lt;strong&gt;published criteria tables&lt;/strong&gt;, I found that the official tool and the official rules disagree:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;TCF Canada at NCLC 4&lt;/strong&gt;: the published table gives 6 points per ability for a first official language at that level, but the tool gives 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;French-first candidates with PTE Core&lt;/strong&gt;: the published rules give the 50-point French bonus when English is CLB 5 or higher on any accepted test, but the tool doesn't recognize PTE Core (a newer test) for this bonus.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I chose to follow the published criteria and document both cases. The test excludes exactly those two cases. Every other profile has to match to the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping it honest over time
&lt;/h2&gt;

&lt;p&gt;Rules change, so the checks run automatically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On every push&lt;/strong&gt;, CI runs the 5,000-profile comparison.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every day&lt;/strong&gt;, a GitHub Action downloads IRCC's criteria page and the calculator script. If either changes, it re-runs the comparison against the &lt;em&gt;new&lt;/em&gt; script and opens an issue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every 6 hours&lt;/strong&gt;, another job fetches the official draw data, so the site always shows the latest cut-offs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The result
&lt;/h2&gt;

&lt;p&gt;The calculator is at &lt;strong&gt;&lt;a href="https://crs-calculator.com/" rel="noopener noreferrer"&gt;crs-calculator.com&lt;/a&gt;&lt;/strong&gt;. It's a static Astro site with no backend: answers never leave the browser. Because it uses IRCC's published pool distribution, it can also show something the official tool doesn't: &lt;strong&gt;roughly where your score ranks among the ~230,000 candidates in the pool&lt;/strong&gt;. It also compares your score with the latest cut-off for each draw type.&lt;/p&gt;

&lt;p&gt;If you're building any calculator based on government rules (tax, benefits, immigration), I can't recommend the oracle approach enough. If the official implementation is public, test against it. Your hand-written cases are only as good as your understanding of the rules, and the oracle doesn't depend on that.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>testing</category>
      <category>node</category>
      <category>sideprojects</category>
    </item>
  </channel>
</rss>
