<?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: Olga Dimitrova</title>
    <description>The latest articles on DEV Community by Olga Dimitrova (@olgadimitrova).</description>
    <link>https://dev.to/olgadimitrova</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%2F4147933%2Fb51b277d-ee9a-4636-94aa-921038a31c08.png</url>
      <title>DEV Community: Olga Dimitrova</title>
      <link>https://dev.to/olgadimitrova</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/olgadimitrova"/>
    <language>en</language>
    <item>
      <title>One salary review, three implementations, one referee</title>
      <dc:creator>Olga Dimitrova</dc:creator>
      <pubDate>Wed, 14 Oct 2026 20:31:10 +0000</pubDate>
      <link>https://dev.to/olgadimitrova/one-salary-review-three-implementations-one-referee-1aag</link>
      <guid>https://dev.to/olgadimitrova/one-salary-review-three-implementations-one-referee-1aag</guid>
      <description>&lt;p&gt;I built a salary review cost calculator for compensation teams. First it closes the part of the gender pay gap that grade and tenure don't explain. Then it works out merit increases person by person and shows what the review costs this year and in next year's budget. The page runs in the browser and makes no network requests.&lt;/p&gt;

&lt;p&gt;Live page: &lt;a href="https://olgadimitrovagit.github.io/comp-budget-lab/merit-matrix-calculator/" rel="noopener noreferrer"&gt;https://olgadimitrovagit.github.io/comp-budget-lab/merit-matrix-calculator/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Reward teams check numbers in Excel, so the calculator also ships as a workbook with live formulas, one row per employee. That gives me the same rules written three times: a JavaScript engine behind the page, Excel formulas, and a Python reference. If the page and the workbook disagree by a euro, the reader stops trusting both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The referee
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;reference_calc.py&lt;/code&gt; is plain Python with no interface. The page and the workbook are never compared with each other. Each is compared with the reference.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The workbook. &lt;code&gt;build_model.py&lt;/code&gt; writes it with openpyxl. openpyxl stores formulas but not their results, so a freshly built file has no computed values at all. I recalculate it in real Excel. Then &lt;code&gt;check_model.py&lt;/code&gt; reads the values back and fails on any cell that starts with &lt;code&gt;#&lt;/code&gt;. It also compares the summary with the reference: one cent for money, 1e-6 for shares.&lt;/li&gt;
&lt;li&gt;The page. &lt;code&gt;check-engine.mjs&lt;/code&gt; runs the JavaScript engine and the reference on five scenarios: the demo with default settings, a January review with no cap, a December 2028 review with custom rating targets, a November 2026 review with different contribution rates, and every eighth employee of the demo. It compares 5,645 values and finds no differences.&lt;/li&gt;
&lt;li&gt;The check of the check. &lt;code&gt;check-engine.mjs --selftest&lt;/code&gt; changes one rate on purpose and expects to fail. It does, with 16 differences. A check that passed after that change would also miss a real error.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the referee missed
&lt;/h2&gt;

&lt;p&gt;While reviewing the workbook, I changed the review date on the Inputs sheet and the calculation broke.&lt;/p&gt;

&lt;p&gt;Eligibility depends on complete months since hire and since the last pay change. In Excel that is &lt;code&gt;DATEDIF&lt;/code&gt;, and &lt;code&gt;DATEDIF&lt;/code&gt; returns &lt;code&gt;#NUM!&lt;/code&gt; when the start date is later than the end date. Some demo employees had a hire or pay change date after the date I picked. The error ran along their rows and from there into the totals.&lt;/p&gt;

&lt;p&gt;The Python reference has the same rule and no such problem. Its month count just goes negative. A negative count is below the minimum, so the person is not eligible. The two implementations agreed on every input the checks used, because none of those inputs had a date after the review date. The reference only catches differences on the data you run through it.&lt;/p&gt;

&lt;p&gt;The fix was small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=IF(hire &amp;gt; EffDate, -1, DATEDIF(hire, EffDate, "m"))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A date after the review date now means "not eligible for merit" in all three places. The demo data no longer has dates after October 2026, and the review date is picked from a list that starts in November 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd keep
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;One reference that both the page and the workbook are checked against, instead of comparing them with each other.&lt;/li&gt;
&lt;li&gt;Real Excel for the recalculation. Without it the workbook check reads empty cells.&lt;/li&gt;
&lt;li&gt;A self-test for every check.&lt;/li&gt;
&lt;li&gt;Edge cases from the person who will use the tool. Mine showed up when I changed one date, which is the first thing a compensation manager does with a workbook like this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data is synthetic: 212 employees, Spain, salary ranges for 2026, contribution rates for Spain 2026.&lt;/p&gt;

&lt;p&gt;Repo, workbook, sample CSV files, reference and checks: &lt;a href="https://github.com/OlgaDimitrovaGit/comp-budget-lab/tree/main/merit-matrix-calculator" rel="noopener noreferrer"&gt;https://github.com/OlgaDimitrovaGit/comp-budget-lab/tree/main/merit-matrix-calculator&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>excel</category>
      <category>javascript</category>
      <category>testing</category>
    </item>
    <item>
      <title>Demo data that has to raise an alarm, without faking the total</title>
      <dc:creator>Olga Dimitrova</dc:creator>
      <pubDate>Thu, 01 Oct 2026 09:40:15 +0000</pubDate>
      <link>https://dev.to/olgadimitrova/demo-data-that-has-to-raise-an-alarm-without-faking-the-total-7cg</link>
      <guid>https://dev.to/olgadimitrova/demo-data-that-has-to-raise-an-alarm-without-faking-the-total-7cg</guid>
      <description>&lt;p&gt;I built a small payroll dashboard: plan against actual for seven departments over twelve months, on one screen, for an HR director who opens it on a phone. Its job is to signal when the year goes over budget. So the demo data had to trip the 3% overspend alarm, and I did not want to get there by setting the total and working backwards.&lt;/p&gt;

&lt;p&gt;Live page: &lt;a href="https://olgadimitrovagit.github.io/comp-budget-lab/budget-plan-fact/" rel="noopener noreferrer"&gt;https://olgadimitrovagit.github.io/comp-budget-lab/budget-plan-fact/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The rule
&lt;/h2&gt;

&lt;p&gt;Each department gets a monthly deviation from plan of 1 to 3 percent, always in the same direction. The company total is never set. It is whatever the departments add up to. The generator is seeded, so it gives the same CSV on every run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it broke
&lt;/h2&gt;

&lt;p&gt;The rule alone could not produce the alarm. I tried three times:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Round&lt;/th&gt;
&lt;th&gt;What I tried&lt;/th&gt;
&lt;th&gt;Year&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;All departments inside ±1-3%&lt;/td&gt;
&lt;td&gt;+0.82%. Engineering is 24% of payroll and underspends, which cancelled out the rest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Engineering at the bottom of its range, everyone else at the top&lt;/td&gt;
&lt;td&gt;+2.16% at most, still under 3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Let one department, Sales, break out at 9-10%&lt;/td&gt;
&lt;td&gt;+3.25%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Round 3 is tuning, and I would rather say so. I didn't set the total, but I chose which department breaks out knowing what it would do to the year. It is also how I've seen payroll budgets fail in practice: in one place, not evenly. The exception is named in the code, and a check fails if a second department leaves the range.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug no test caught
&lt;/h2&gt;

&lt;p&gt;From September the forecast adds a pay equalisation: €33,020 a month across 52 of 212 employees. Reading the output, I asked a simple question: why does the fourth quarter show a smaller overspend, when nothing is saved there and only a pay rise is added?&lt;/p&gt;

&lt;p&gt;The generator had &lt;code&gt;forecast = plan + uplift&lt;/code&gt;, so every department snapped back to plan in September. It should have been the department's own trend plus the uplift. All eight checks passed, and one of them even confirmed the wrong formula. I build these tools with an AI coding agent, and the checks were written together with the code, so they shared its blind spot. What caught it was a question from someone who knows how payroll behaves.&lt;/p&gt;

&lt;p&gt;I rewrote that check and added three: the year must cross the alarm threshold, Sep-Dec must run hotter than Jan-Aug, and exactly one department may leave the range. Putting the old formula back now fails three of the 11 checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Smaller things that held
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The Excel model recalculates in real Excel and matches the CSV to 0.000000 on all five control totals. On the way it was off by 136,081 EUR, then by 17.06 EUR. Both times I fixed the cause instead of widening the tolerance.&lt;/li&gt;
&lt;li&gt;The whole dashboard is one HTML file of about 680 KB, with React and Recharts bundled inside, so it loads nothing from the network.&lt;/li&gt;
&lt;li&gt;After my first post, a reader asked whether my network check also covered what happens after someone uploads a file. It didn't. Now both tools have a check that picks the sample CSV through the real file input and fails on any request. Thank you to that reader.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The data is synthetic, and the contribution rates are Spain 2026.&lt;/p&gt;

&lt;p&gt;Repo, workbook, sample CSV, generator and checks: &lt;a href="https://github.com/OlgaDimitrovaGit/comp-budget-lab/tree/main/budget-plan-fact" rel="noopener noreferrer"&gt;https://github.com/OlgaDimitrovaGit/comp-budget-lab/tree/main/budget-plan-fact&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
      <category>dataviz</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why a pay-gap cost calculator should make zero network calls</title>
      <dc:creator>Olga Dimitrova</dc:creator>
      <pubDate>Wed, 30 Sep 2026 03:47:06 +0000</pubDate>
      <link>https://dev.to/olgadimitrova/why-a-pay-gap-cost-calculator-should-make-zero-network-calls-2n38</link>
      <guid>https://dev.to/olgadimitrova/why-a-pay-gap-cost-calculator-should-make-zero-network-calls-2n38</guid>
      <description>&lt;p&gt;I maintain a small open-source calculator that prices what it costs to close an unexplained gender pay gap under the EU Pay Transparency Directive (2023/970). You give it a CSV of employees (salary, gender, grade, tenure, category), and it prices two options: minimum compliance and full equalisation. It ships as a static HTML page and, separately, as an Excel workbook with the same calculation in live formulas.&lt;/p&gt;

&lt;p&gt;The page makes no network requests at all. It doesn't promise to skip logging or to encrypt your data on a server, because there is no server. Open the browser's Network tab while you use it and it stays empty. Most of the design follows from that.&lt;/p&gt;

&lt;h2&gt;
  
  
  The input is some of the most sensitive data a company holds
&lt;/h2&gt;

&lt;p&gt;A pay-gap calculation needs individual salaries by gender, category, grade and tenure. Most HR and reward teams would rather not let that leave the company, even briefly, even to a vendor with a data processing agreement and a SOC 2 report. A web app that uploads the CSV to a server asks every user to trust a company they have probably never audited, for a calculation their laptop does in milliseconds.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;index.html&lt;/code&gt; is the whole application. HTML, CSS and JavaScript sit in one file. There is no build step to run it and no backend to operate or patch, and nobody's salary file ends up on a server.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "no network calls" requires in practice
&lt;/h2&gt;

&lt;p&gt;"Runs in your browser" is easy to say. Keeping it true means giving up a few conveniences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No CDN for fonts or libraries. A dependency is either vendored into the file or not used. The chart is plain SVG, not a charting library.&lt;/li&gt;
&lt;li&gt;No analytics, error reporting or telemetry, including the "just to see if people use it" kind. Each of those is a network call.&lt;/li&gt;
&lt;li&gt;The CSV comes in through an &lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt; and is read into memory with the FileReader API. The page computes the result, and the data is gone on refresh. Nothing is written anywhere unless the user downloads a result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The repo has two checks for this. &lt;code&gt;check-render.js&lt;/code&gt; scans the built page and fails (exit 1) if it references any external script, stylesheet or image. &lt;code&gt;check-meta.js&lt;/code&gt; opens the page in headless Chrome, records every network request made while it loads, and fails (exit 1) if there is even one. Without a test that can fail, "no network calls" only means "no network calls that we noticed."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Excel workbook exists
&lt;/h2&gt;

&lt;p&gt;If you can't read JavaScript, the web page is hard to verify. Without developer tools, how do you see what it does with your data? So the same calculation also exists as an Excel workbook, all in live formulas: medians by rank, and the OLS regression written out with explicit sums and Cramer's rule instead of an array formula or a macro. Click a cell, read the formula, change an input and watch every downstream number move. There is no VBA and nothing hidden behind a button.&lt;/p&gt;

&lt;p&gt;I cross-checked the web version, the workbook and a third, independent Python port on the demo dataset, and they agree to 1e-9. The port is in the repo at &lt;code&gt;pay-gap-calculator/build/excel/reference_calc.py&lt;/code&gt;. It follows &lt;code&gt;pay-gap-calculator/build/calc.js&lt;/code&gt; step by step in a different language and prints every category, so you can rerun the comparison yourself instead of taking the README's word for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this doesn't solve
&lt;/h2&gt;

&lt;p&gt;Open source and zero network calls answer "can I trust this with my data." They don't answer "is this the right method." The calculator prices the unexplained residual after controlling for grade and tenure (OLS) and remediates only that residual. It doesn't decide whether a gap is lawful, and it doesn't group work of equal value into categories for you. Categories too small to be statistically reliable get flagged rather than hidden. Those are choices, and someone else building the same tool could reasonably make different ones. Whether your salary data should touch a server to find out is a separate question, and I think it has one answer.&lt;/p&gt;

&lt;p&gt;Repo, workbook, sample CSV and the checks: &lt;a href="https://github.com/OlgaDimitrovaGit/comp-budget-lab/tree/main/pay-gap-calculator" rel="noopener noreferrer"&gt;https://github.com/OlgaDimitrovaGit/comp-budget-lab/tree/main/pay-gap-calculator&lt;/a&gt;&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>javascript</category>
      <category>hr</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
