<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Scopefolio]]></title><description><![CDATA[Scopefolio]]></description><link>https://scopefolio.hashnode.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/6abf9facd1a0b41394f7695f/11a66ac7-d4b3-45ec-8233-d3958c31f3bd.png</url><title>Scopefolio</title><link>https://scopefolio.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 14:16:06 GMT</lastBuildDate><atom:link href="https://scopefolio.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Make form errors easier to find and fix in a client portal]]></title><description><![CDATA[By Scopefolio Support. Prepared with AI assistance. Scopefolio is our product. This tutorial uses an illustrative implementation, not a description of…]]></description><link>https://scopefolio.hashnode.dev/make-form-errors-easier-to-find-and-fix-in-a-client-portal</link><guid isPermaLink="true">https://scopefolio.hashnode.dev/make-form-errors-easier-to-find-and-fix-in-a-client-portal</guid><dc:creator><![CDATA[Scopefolio Support]]></dc:creator><pubDate>Mon, 12 Oct 2026 11:29:53 GMT</pubDate><content:encoded><![CDATA[<p>By Scopefolio Support. Prepared with AI assistance.</p>
<p>Scopefolio is our product. This tutorial uses an illustrative implementation, not a description of Scopefolio’s internal code.</p>
<p>Imagine a client writing a change request, pressing Submit, and seeing only a red border. The page has detected a problem, but the client still needs three answers: what failed, where it failed, and how to continue without losing their work.</p>
<p>A useful error-handling flow connects those answers. This small example checks one required request field. It demonstrates feedback and focus-management code locally. It does not save data, call a server, or describe observed Scopefolio behavior.</p>
<h2>Start with a named control</h2>
<p>Give the input a visible label whose <code>for</code> matches its <code>id</code>. Keep instructions visible after typing begins, rather than hiding them in a placeholder. W3C's <a href="https://www.w3.org/WAI/tutorials/forms/labels/">labeling guidance</a> explains how that association helps people identify and activate the control.</p>
<p>The example treats whitespace-only text as empty. It waits for submission before displaying an error, so someone composing a request does not receive premature warnings.</p>
<h2>Connect the summary to the field</h2>
<p>Save the following as <code>form-demo.html</code> and open it in a browser. It has no dependencies or network requests. The button starts disabled so this demonstration cannot submit without its JavaScript handler.</p>
<pre><code class="language-html">&lt;!doctype html&gt;
&lt;html lang="en"&gt;
&lt;meta charset="utf-8"&gt;
&lt;meta name="viewport" content="width=device-width, initial-scale=1"&gt;
&lt;title&gt;Change request demo&lt;/title&gt;
&lt;style&gt;
  body { max-width: 42rem; margin: 2rem auto; padding: 1rem; }
  input, button { font: inherit; padding: .5rem; }
  input { display: block; max-width: 100%; box-sizing: border-box; }
  :focus-visible, #errors:focus { outline: 3px solid; outline-offset: 4px; }
&lt;/style&gt;
&lt;main&gt;
  &lt;h1&gt;Describe a project change&lt;/h1&gt;
  &lt;section id="errors" tabindex="-1" aria-labelledby="error-title" hidden&gt;
    &lt;h2 id="error-title"&gt;There is a problem&lt;/h2&gt;
    &lt;ul&gt;&lt;li&gt;&lt;a id="error-link" href="#request"&gt;&lt;/a&gt;&lt;/li&gt;&lt;/ul&gt;
  &lt;/section&gt;
  &lt;form id="request-form"&gt;
    &lt;label for="request"&gt;Requested change (required)&lt;/label&gt;
    &lt;p id="request-hint"&gt;Describe the change you want reviewed.&lt;/p&gt;
    &lt;input id="request" name="request" required
           aria-describedby="request-hint"&gt;
    &lt;p id="request-error" hidden&gt;&lt;/p&gt;
    &lt;button disabled&gt;Check request&lt;/button&gt;
  &lt;/form&gt;
  &lt;p id="result" role="status"&gt;&lt;/p&gt;
  &lt;noscript&gt;This demonstration needs JavaScript. Nothing has been sent.&lt;/noscript&gt;
&lt;/main&gt;
&lt;script&gt;
  const byId = id =&gt; document.getElementById(id);
  const form = byId('request-form');
  const field = byId('request');
  const summary = byId('errors');
  const error = byId('request-error');
  const link = byId('error-link');
  const result = byId('result');

  link.addEventListener('click', event =&gt; {
    event.preventDefault();
    field.focus();
  });

  form.addEventListener('submit', event =&gt; {
    event.preventDefault();
    result.textContent = '';
    const message = field.value.trim()
      ? '' : 'Requested change: describe the change you want reviewed.';
    error.textContent = message;
    error.hidden = !message;
    summary.hidden = !message;
    link.textContent = message;
    if (message) {
      field.setAttribute('aria-invalid', 'true');
      field.setAttribute('aria-describedby', 'request-hint request-error');
      summary.focus();
    } else {
      field.removeAttribute('aria-invalid');
      field.setAttribute('aria-describedby', 'request-hint');
      result.textContent = 'Checks passed. Demo only: nothing was sent.';
    }
  });
  form.noValidate = true;
  form.querySelector('button').disabled = false;
&lt;/script&gt;
&lt;/html&gt;
</code></pre>
<p>On failure, the script requests focus on the summary. Its link calls <code>focus()</code> on the affected input. The inline message uses the same wording, and <code>aria-describedby</code> retains the instruction while adding the error. Values are never cleared. The input receives <code>aria-invalid</code> only after a failed check. Errors remain visible while the person edits; validation runs again on the next submission. Once a corrected value passes, the script removes that state and the error reference together, leaving the original hint in place. Hiding an old paragraph while keeping stale validation state can otherwise leave the visual form and its accessibility information describing different situations.</p>
<p>A summary becomes especially useful when a longer form has several errors. Add one link per invalid field in page order. W3C's <a href="https://www.w3.org/WAI/tutorials/forms/notifications/">notification guidance</a> covers linked summaries and inline feedback; the <a href="https://design-system.service.gov.uk/components/error-summary/">GOV.UK error-summary component</a> also documents moving keyboard focus to the summary.</p>
<p>This example attempts to notify users by moving focus, without adding an assertive live region to the same summary. W3C’s notification tutorial also describes an alert-region approach for dynamically inserted error lists. This simplified example is not a reproduction of the GOV.UK component. Test whether its focused section and linked error are announced clearly in your supported browser and screen-reader combinations before adopting it. On a successful check, the existing <code>role="status"</code> region changes without moving focus. That role has implicit polite announcement behavior, as documented by <a href="https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/status_role">MDN</a>.</p>
<h2>Choose who owns validation feedback</h2>
<p>Native HTML validation is a reasonable starting point: <code>required</code>, suitable input types, and browser feedback need little code. A custom summary gives the application control over wording and the complete error list, but also makes the application responsible for presenting and updating it.</p>
<p>Here, <code>noValidate</code> suppresses native interactive validation after the handler is installed. Otherwise, the browser could stop an invalid submission before the custom submit handler runs. It does not remove the need for server validation. MDN explains these distinctions in its <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Guides/Constraint_validation">constraint-validation guide</a>.</p>
<p>For a real endpoint, validate again on the server and enforce authorization for every request. OWASP’s <a href="https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html">authorization guidance</a> explains why client-side checks cannot make that decision. Preserve entered values on failure, render field errors through the same pattern, and announce success only after a confirmed save. A network failure needs its own message and a safe retry path; do not label it a malformed request field.</p>
<h2>Verify the recovery path</h2>
<p>Nine local JavaScript tests passed using a simulated DOM: empty and whitespace submissions, focus requests, the summary-link handler, retained input, removal of stale errors, and repeated validation. This checks the script’s state transitions, not actual browser focus or spoken announcements.</p>
<p>The attempted browser verification was blocked at browser launch by the test environment. Manual keyboard and screen-reader testing, zoom/reflow review, and a full accessibility audit were not performed. This example makes no WCAG conformance claim. Before shipping, verify the spoken label and error, visible keyboard focus, repeated submissions, and recovery from server failures in the actual portal.</p>
]]></content:encoded></item><item><title><![CDATA[Exact USD arithmetic in JavaScript with BigInt]]></title><description><![CDATA[By Scopefolio Support. Scopefolio is our product. Prepared with AI assistance. This standalone example parses USD decimal strings, adds integer cents, and…]]></description><link>https://scopefolio.hashnode.dev/exact-usd-arithmetic-in-javascript-with-bigint</link><guid isPermaLink="true">https://scopefolio.hashnode.dev/exact-usd-arithmetic-in-javascript-with-bigint</guid><dc:creator><![CDATA[Scopefolio Support]]></dc:creator><pubDate>Mon, 12 Oct 2026 10:28:47 GMT</pubDate><content:encoded><![CDATA[<p>By Scopefolio Support. Scopefolio is our product. Prepared with AI assistance.</p>
<p>This standalone example parses USD decimal strings, adds integer cents, and formats the result. It is a software tutorial, not financial or accounting advice, and it does not describe Scopefolio’s implementation.</p>
<p>A small total makes the goal concrete. Save the functions below as <code>money.mjs</code>, then put this in <code>example.mjs</code>:</p>
<pre><code class="language-javascript">import { parseUsd, formatUsd } from './money.mjs';

const total = parseUsd('19.99') + parseUsd('0.10') + parseUsd('0.20');
console.log(total.toString());
console.log(formatUsd(total));
</code></pre>
<p>The output is <code>2029</code>, followed by <code>USD 20.29</code>. The amount stays exact from input to display, including when its cent count exceeds JavaScript’s safe-integer range.</p>
<h2>Choose an input contract first</h2>
<p>JavaScript’s <code>Number</code> uses binary floating point. Decimal fractions can lose precision: <code>0.1 + 0.2</code> evaluates to <code>0.30000000000000004</code>. <a href="https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-number-type">ECMAScript defines the Number representation</a>.</p>
<p>Integer cents are useful, but a <code>Number</code> still has a limit. Its maximum safe integer is <code>9007199254740991</code>; larger integers are not all distinguishable. <a href="https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-number.max_safe_integer">The specification documents that boundary</a>. This example uses <code>BigInt</code> so integer addition stays exact beyond it.</p>
<p>The parser accepts an optional minus sign, ASCII digits, a decimal point, and exactly two fractional digits. Examples are <code>"0.10"</code>, <code>"19.99"</code>, and <code>"-2.50"</code>. It rejects whitespace, grouping separators, exponent notation, leading zeros, and extra decimal places. Inputs must already be strings: converting a rounded number to a string cannot recover its original value.</p>
<p>This strict grammar suits an API boundary. A user-facing form may need a separate normalization step with clear validation messages. Keeping that step separate lets the calculation layer receive one predictable format.</p>
<h2>Parse once, then calculate in cents</h2>
<pre><code class="language-javascript">export function parseUsd(text) {
  if (typeof text !== 'string') throw new TypeError('Expected a string');
  if (text.length &gt; 40) throw new RangeError('Amount is too long');
  const m = /^(-?)(0|[1-9][0-9]*)\.([0-9]{2})$/.exec(text);
  if (!m) throw new SyntaxError('Expected USD decimal');
  const cents = BigInt(m[2]) * 100n + BigInt(m[3]);
  return m[1] ? -cents : cents;
}

export function formatUsd(cents) {
  if (typeof cents !== 'bigint') throw new TypeError('Expected bigint');
  const sign = cents &lt; 0n ? '-' : '';
  const magnitude = cents &lt; 0n ? -cents : cents;
  const whole = magnitude / 100n;
  const fraction = (magnitude % 100n).toString().padStart(2, '0');
  return `USD ${sign}${whole}.${fraction}`;
}
</code></pre>
<p>The parser converts the whole and fractional digit groups separately. <code>"19.99"</code> becomes <code>19n * 100n + 99n</code>, or <code>1999n</code>. It never calls <code>parseFloat</code>, multiplies a decimal <code>Number</code>, or guesses a rounding rule. The 40-character limit is an example input-size guard, not a USD rule; choose bounds appropriate to your application.</p>
<p>Use bigint literals such as <code>100n</code> consistently in arithmetic. <a href="https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-bigint">BigInt represents integers and defines exact addition</a>. It cannot represent a fractional cent. The parser therefore rejects <code>"1.005"</code> rather than silently choosing whether it means 100 or 101 cents.</p>
<p>The formatter separates the sign before dividing, then pads the remainder to two digits. That keeps <code>-1n</code> readable as <code>USD -0.01</code>. An input of <code>"-0.00"</code> becomes <code>0n</code> and displays as <code>USD 0.00</code>.</p>
<h2>Test the boundaries</h2>
<p>Save this as <code>money.test.mjs</code> alongside the other files:</p>
<pre><code class="language-javascript">import test from 'node:test';
import assert from 'node:assert/strict';
import { parseUsd, formatUsd } from './money.mjs';

test('adds exact cents', () =&gt; {
  const total = parseUsd('19.99') + parseUsd('0.10') + parseUsd('0.20');
  assert.equal(total, 2029n);
  assert.equal(formatUsd(total), 'USD 20.29');
});

test('preserves cents beyond the Number safe-integer limit', () =&gt; {
  const cents = parseUsd('90071992547409.93');
  assert.equal(cents, 9007199254740993n);
  assert.equal(formatUsd(cents), 'USD 90071992547409.93');
});

test('handles negatives and normalizes negative zero', () =&gt; {
  assert.equal(formatUsd(parseUsd('-0.01')), 'USD -0.01');
  assert.equal(formatUsd(parseUsd('-0.00')), 'USD 0.00');
});

test('rejects malformed input and wrong types', () =&gt; {
  for (const value of ['', '1', '1.0', '01.00', '+1.00', ' 1.00',
    '1.00\n', '1,000.00', '1e2', '1.001']) {
    assert.throws(() =&gt; parseUsd(value), SyntaxError);
  }
  assert.throws(() =&gt; parseUsd(1), TypeError);
  assert.throws(() =&gt; parseUsd('9'.repeat(41)), RangeError);
  assert.throws(() =&gt; formatUsd(100), TypeError);
});

test('round-trips every cent from -100.00 through 100.00', () =&gt; {
  for (let cents = -10000n; cents &lt;= 10000n; cents++) {
    assert.equal(parseUsd(formatUsd(cents).slice(4)), cents);
  }
});
</code></pre>
<p>Run <code>node example.mjs</code> and <code>node --test money.test.mjs</code>. These commands use <a href="https://nodejs.org/api/test.html#running-tests-from-the-command-line">Node’s built-in test runner</a>, so no package installation is needed.</p>
<p>The example and this exact suite were executed on Node v26.11.1. All five tests passed, with zero failures or skips. The final test checked 20,001 round trips. This verifies the listed behavior in that runtime; it does not establish that every money-related requirement is covered.</p>
<p>The large-value case matters because ordinary-looking examples can pass even when a later conversion loses precision. The invalid-input cases also document decisions: <code>"1.0"</code> is rejected intentionally, and a trailing newline is not silently ignored.</p>
<h2>Keep currency, display, and rounding explicit</h2>
<p><code>formatUsd</code> produces a fixed ASCII representation without locale-specific separators. For localized display, current <code>Intl.NumberFormat</code> semantics can preserve decimal-string input without first converting it to <code>Number</code>. <a href="https://tc39.es/ecma402/#sec-tointlmathematicalvalue">ECMA-402 specifies that conversion</a>. Test the behavior in your target runtime, and avoid introducing a lossy <code>Number(cents)</code> conversion just to format a result.</p>
<p>At an API or database boundary, preserve both the amount and its unit. For example, a record might store a currency code of <code>USD</code> and a decimal-string cent count of <code>"2029"</code>. Define a matching validation rule when reading that record back; it is a different format from the dollar input accepted here. Never infer the unit from the number alone: 2029 cents and 2029 dollars are very different values.</p>
<p>The factor of 100 belongs to this USD contract. Other currencies can use different fractional-digit counts; <a href="https://tc39.es/ecma402/#sec-currencydigits">ECMA-402 treats currency digits separately</a>. A multi-currency system should carry a currency code with each amount and reject addition across currencies unless an explicit conversion has occurred.</p>
<p>Finally, percentages and allocation need their own rules. <a href="https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-numeric-types-bigint-divide">BigInt division truncates toward zero</a>, so splitting 100 cents three ways produces a remainder. Decide how to round and distribute that remainder before implementing taxes, discounts, or splits.</p>
<p>For this limited workflow, the invariant is simple: validated USD strings enter, bigint cents are added, and formatting preserves every cent. Keep that invariant visible in both the API and the tests.</p>
]]></content:encoded></item><item><title><![CDATA[Handle duplicate project webhooks with one SQLite transaction]]></title><description><![CDATA[By Scopefolio Support. Prepared with AI assistance. A project integration receives a completion event, saves the new status, and loses its HTTP response on…]]></description><link>https://scopefolio.hashnode.dev/handle-duplicate-project-webhooks-with-one-sqlite-transaction</link><guid isPermaLink="true">https://scopefolio.hashnode.dev/handle-duplicate-project-webhooks-with-one-sqlite-transaction</guid><dc:creator><![CDATA[Scopefolio Support]]></dc:creator><pubDate>Mon, 12 Oct 2026 07:12:50 GMT</pubDate><content:encoded><![CDATA[<p>By Scopefolio Support. Prepared with AI assistance.</p>
<p>A project integration receives a completion event, saves the new status, and loses its HTTP response on the way back to the sender. The sender retries. Your endpoint now has to recognize work it already committed.</p>
<p>Recording an event ID helps, but the receipt and project update need to succeed together. Saving the receipt first can lose work after a crash. Saving it afterward can allow the same work twice.</p>
<h2>Define the guarantees before the handler</h2>
<p>This illustrative integration receives immutable events containing a project snapshot and an increasing integer revision. A revision identifies one project state. The trusted connection key identifies the provider and source account; project IDs and event IDs are unique within that connection.</p>
<p>Those are requirements of this example, not universal webhook guarantees. <a href="https://docs.stripe.com/webhooks#event-delivery-behaviors">Stripe's webhook documentation</a>, for example, warns about both duplicate deliveries and unordered events. An event timestamp is not a reliable substitute for a resource revision.</p>
<p>Save this as <code>store.mjs</code>. It uses Node's <a href="https://nodejs.org/api/sqlite.html">built-in SQLite module</a>, without installing packages:</p>
<pre><code class="language-javascript">import {DatabaseSync} from "node:sqlite";

export function store() {
  const db = new DatabaseSync(":memory:");
  db.exec(`
    CREATE TABLE receipts (
      connection TEXT NOT NULL, id TEXT NOT NULL,
      PRIMARY KEY(connection, id)
    );
    CREATE TABLE projects (
      connection TEXT NOT NULL, id TEXT NOT NULL,
      version INTEGER NOT NULL,
      status TEXT NOT NULL CHECK(status IN ('active', 'done')),
      PRIMARY KEY(connection, id)
    );
  `);
  const receipt = db.prepare(`INSERT INTO receipts VALUES (?, ?)
    ON CONFLICT(connection, id) DO NOTHING`);
  const update = db.prepare(`INSERT INTO projects VALUES (?, ?, ?, ?)
    ON CONFLICT(connection, id) DO UPDATE SET
      version = excluded.version, status = excluded.status
    WHERE excluded.version &gt; projects.version`);

  function apply(e) {
    if (!e || ![e.connection, e.id, e.project, e.status].every(
      value =&gt; typeof value === "string" &amp;&amp; value.length &gt; 0) ||
      !Number.isSafeInteger(e.version) || e.version &lt; 1) {
      throw new Error("Invalid event");
    }
    db.exec("BEGIN IMMEDIATE");
    try {
      let result = "duplicate";
      if (receipt.run(e.connection, e.id).changes) {
        result = update.run(e.connection, e.project, e.version, e.status)
          .changes ? "applied" : "stale";
      }
      db.exec("COMMIT");
      return result;
    } catch (error) {
      db.exec("ROLLBACK");
      throw error;
    }
  }
  return {db, apply};
}
</code></pre>
<h2>Keep two decisions separate</h2>
<p>The receipt's unique key handles repeated deliveries of one event. The conditional update handles a different event that describes an older or equal revision. Both are necessary. A previously unseen event can still contain stale state.</p>
<p>The receipt remains committed for a stale event because processing it is complete. A failed update rolls back its new receipt, allowing a retry after the underlying problem is fixed. SQLite documents these <a href="https://www.sqlite.org/lang_transaction.html">transaction boundaries</a> and the conditional behavior of <a href="https://www.sqlite.org/lang_upsert.html">UPSERT</a>.</p>
<h2>Exercise the failure path</h2>
<p>Save this as <code>store.test.mjs</code>:</p>
<pre><code class="language-javascript">import assert from "node:assert/strict";
import test from "node:test";
import {store} from "./store.mjs";

test("duplicate, stale, isolated, and failed deliveries", () =&gt; {
  const {db, apply} = store();
  const e = {connection: "tracker:team-a", id: "e2",
    project: "p1", version: 2, status: "done"};
  try {
    assert.equal(apply(e), "applied");
    assert.equal(apply(e), "duplicate");
    assert.equal(apply({...e, id: "e1", version: 1, status: "active"}), "stale");
    assert.equal(db.prepare("SELECT status FROM projects").get().status, "done");
    assert.equal(apply({...e, connection: "tracker:team-b"}), "applied");
    assert.throws(() =&gt; apply({...e, id: "e3", version: 3, status: "invalid"}));
    assert.equal(db.prepare("SELECT count(*) AS n FROM receipts WHERE id='e3'")
      .get().n, 0);
    assert.equal(apply({...e, id: "e3", version: 3, status: "active"}), "applied");
  } finally {
    db.close();
  }
});
</code></pre>
<p>Run <code>node --test store.test.mjs</code>. This exact example passed its test and eight assertions on Node v24.19.0. The test uses memory storage and does not demonstrate durability across process crashes or concurrent workers.</p>
<h2>Connect this core carefully</h2>
<p>Verify the provider's signature using its documented method before creating trusted input. For Stripe, verification needs the unmodified request body. Validate the event type and schema, and resolve the connection from authenticated configuration. An event ID cannot authenticate a sender. <a href="https://docs.stripe.com/webhooks#verify-webhook-signatures-with-official-libraries">Stripe's verification guidance</a> explains that boundary.</p>
<p>A deployed handler needs persistent storage, migrations, lock-contention handling, and a receipt-retention policy covering supported replays. Acknowledge success only after this short transaction, or after durably queuing the event for later processing.</p>
<p>Keep the connection namespace in both tables. Two independent customer accounts may each have a project named <code>p1</code> and an event named <code>e2</code>. A globally unique receipt key alone would let the first account suppress the second account's update. The isolation assertion exercises this exact collision.</p>
<p>Log the event ID, connection, and result without storing unnecessary payload data. Repeated <code>duplicate</code> results can be normal; database errors need investigation. If one revision ever arrives with conflicting state, the source has violated this model's assumptions. Flag that inconsistency rather than silently choosing whichever payload arrived first.</p>
<p>If a provider supplies no reliable revision, use a provider-specific reconciliation strategy instead of inventing ordering from arrival time. For notifications or other external effects, write an outbox job in the same transaction and make its consumer idempotent too. A database rollback cannot unsend an email.</p>
]]></content:encoded></item><item><title><![CDATA[Derive invoice status from ledger entries]]></title><description><![CDATA[By Scopefolio Support. Prepared with AI assistance. An invoice is marked paid. A refund arrives, but the label stays unchanged because the refund handler…]]></description><link>https://scopefolio.hashnode.dev/derive-invoice-status-from-ledger-entries</link><guid isPermaLink="true">https://scopefolio.hashnode.dev/derive-invoice-status-from-ledger-entries</guid><dc:creator><![CDATA[Scopefolio Support]]></dc:creator><pubDate>Sun, 11 Oct 2026 10:16:09 GMT</pubDate><content:encoded><![CDATA[<p>By Scopefolio Support. Prepared with AI assistance.</p>
<p>An invoice is marked paid. A refund arrives, but the label stays unchanged because the refund handler never updates it. Every screen now has to recalculate the balance.</p>
<p>Record invoice, payment, and refund entries, then derive payment status from the billed amount and net payments. This is an illustrative software model, not accounting guidance.</p>
<h2>Make the model explicit</h2>
<p>This example handles one positive-total invoice in USD, with amounts in integer cents. Entries arrive in ledger order: issuance first, and each refund after its referenced payment. Only confirmed payment and refund events belong here. Pending attempts, fees, credit notes, cancellation, tax calculation, and currency conversion are outside the model.</p>
<p>A refund reduces net payments without changing the billed amount. Therefore, a refund can reopen the balance. A full refund produces <code>unpaid</code>, while the separate refund total preserves the history. If your workflow also reduces the billed amount, add an explicit adjustment model before using these statuses for customer-facing decisions.</p>
<p>The four statuses are <code>unpaid</code> when net payments are zero, <code>partial</code> when they are below the invoice total, <code>paid</code> at equality, and <code>overpaid</code> above it. A negative balance means excess payment; it must remain visible.</p>
<h2>Encode the invariants</h2>
<p><a href="https://tc39.es/ecma262/multipage/ecmascript-data-types-and-values.html#sec-ecmascript-language-types-bigint-type">JavaScript's BigInt specification</a> defines integer arithmetic without a fixed bit-width. Here, <code>10000n</code> means 10,000 cents. The reducer accepts positive BigInt amounts directly; text parsing and display formatting belong at the boundaries.</p>
<p>Save this as <code>ledger.mjs</code>:</p>
<pre><code class="language-javascript">export function summarize(entries) {
  const invoice = entries[0];
  if (invoice?.type !== 'invoice' || typeof invoice.invoiceId !== 'string' || !invoice.invoiceId)
    throw Error('Invoice must be first');
  const ids = new Set();
  const refundable = new Map();
  let billed = 0n, paid = 0n, refunded = 0n;

  for (const e of entries) {
    if (typeof e.id !== 'string' || !e.id || ids.has(e.id))
      throw Error('Invalid or duplicate entry ID');
    if (e.currency !== 'USD' || e.invoiceId !== invoice.invoiceId)
      throw Error('Wrong currency or invoice');
    if (typeof e.amount !== 'bigint' || e.amount &lt;= 0n)
      throw Error('Amount must be positive BigInt cents');
    ids.add(e.id);

    if (e.type === 'invoice' &amp;&amp; billed === 0n) {
      billed = e.amount;
    } else if (e.type === 'payment') {
      paid += e.amount;
      refundable.set(e.id, e.amount);
    } else if (e.type === 'refund') {
      const available = refundable.get(e.paymentId) ?? 0n;
      if (e.amount &gt; available) throw Error('Invalid refund');
      refundable.set(e.paymentId, available - e.amount);
      refunded += e.amount;
    } else {
      throw Error('Unknown type or second invoice');
    }
  }

  const netPaid = paid - refunded;
  const balance = billed - netPaid;
  const status = netPaid === 0n ? 'unpaid'
    : balance &gt; 0n ? 'partial'
    : balance === 0n ? 'paid' : 'overpaid';
  return { status, billed, paid, refunded, netPaid, balance };
}
</code></pre>
<p>The important checks are structural. Each entry has a unique ID, belongs to the same invoice and currency, and has a supported type. Refunds consume the remaining refundable amount of a specific payment. Checking only total refunds against total payments would allow one payment to be over-refunded while another hides the mistake.</p>
<p>Returning totals alongside the label lets screens explain results and tests verify the arithmetic: <code>balance = billed - paid + refunded</code>.</p>
<h2>Exercise the transitions</h2>
<p>Save this as <code>ledger.test.mjs</code> beside the reducer. The fixture issues a $100 invoice, receives $100, and refunds $25, producing a partial payment status with a $25 balance.</p>
<pre><code class="language-javascript">import test from 'node:test';
import assert from 'node:assert/strict';
import { summarize } from './ledger.mjs';

const entry = (id, type, amount, paymentId) =&gt; ({
  id, type, amount, paymentId, invoiceId: 'inv-1', currency: 'USD'
});
const invoice = entry('i1', 'invoice', 10000n);
const payment = entry('p1', 'payment', 10000n);
const refund = entry('r1', 'refund', 2500n, 'p1');
const fixture = [invoice, payment, refund];

test('fixture reopens a paid balance', () =&gt; {
  assert.deepEqual(summarize(fixture), {
    status: 'partial', billed: 10000n, paid: 10000n,
    refunded: 2500n, netPaid: 7500n, balance: 2500n
  });
});

test('all four statuses', () =&gt; {
  for (const [amount, status] of [
    [0n, 'unpaid'], [1n, 'partial'],
    [10000n, 'paid'], [10001n, 'overpaid']
  ]) {
    const rows = amount ? [invoice, { ...payment, amount }] : [invoice];
    assert.equal(summarize(rows).status, status);
  }
});

test('rejects duplicate IDs and excessive cumulative refunds', () =&gt; {
  assert.throws(() =&gt; summarize([...fixture, payment]), /duplicate/);
  const excess = entry('r2', 'refund', 7501n, 'p1');
  assert.throws(() =&gt; summarize([...fixture, excess]), /Invalid refund/);
});
</code></pre>
<p>Run <code>node --test ledger.test.mjs</code> using <a href="https://nodejs.org/docs/latest-v24.x/api/test.html">Node's built-in test runner</a>. These three tests passed on Node.js v24.19.0. They cover the fixture, every status, duplicate records, and cumulative refund limits. They do not exercise a database or payment provider.</p>
<h2>Protect the write path too</h2>
<p>This pure function validates a snapshot. Two concurrent refund requests can each read the same available amount and both pass individually. The persistence layer must serialize the relevant check and append, using a transaction with suitable locking or a version check that rejects stale writes. Otherwise, a correct reducer simply discovers the invalid history afterward.</p>
<p>Keep ledger records append-only through your write API. Model corrections with explicit events and documented effects rather than editing old amounts. At ingestion, use a stable provider event identifier with a database uniqueness constraint. Receiving the same event twice should not create two ledger rows; conflicting payloads for one identifier deserve investigation.</p>
<p>A stored status can be a cache: rebuild it from the ledger, associate it with the ledger version, and test replay. During migration, compare derived results with existing labels and investigate differences before changing screens.</p>
]]></content:encoded></item><item><title><![CDATA[Design a Dry-Run Import That Cannot Write Client Records]]></title><description><![CDATA[By Scopefolio Support. Prepared with AI assistance. An import preview should answer a practical question: what would change if this file were accepted? A…]]></description><link>https://scopefolio.hashnode.dev/design-a-dry-run-import-that-cannot-write-client-records</link><guid isPermaLink="true">https://scopefolio.hashnode.dev/design-a-dry-run-import-that-cannot-write-client-records</guid><dc:creator><![CDATA[Scopefolio Support]]></dc:creator><pubDate>Sun, 11 Oct 2026 10:03:18 GMT</pubDate><content:encoded><![CDATA[<p>By Scopefolio Support. Prepared with AI assistance.</p>
<p>An import preview should answer a practical question: what would change if this file were accepted? A reassuring “dry run” label provides little protection when the same function can update records, send notifications, and then attempt a rollback.</p>
<p>A clearer design separates planning from execution. Give the planner a current snapshot and staged candidate records. It returns proposed changes and validation errors. It has no database connection, filesystem access, or network client.</p>
<p>Here, staging means holding a parsed candidate in memory. Nothing is persisted. All fixture records are invented, and the implementation is an independent teaching example.</p>
<h2>Define the import policy</h2>
<p>This miniature importer handles records with exactly two fields: <code>id</code> and <code>name</code>. IDs follow a deliberately narrow pattern, such as <code>A01</code>. Names must contain non-whitespace text and stay within 100 UTF-16 code units. Each input array is limited to 1,000 records; these are example limits rather than capacity measurements.</p>
<p>An existing ID can change its name. A new ID becomes a proposed creation. Identical records produce no action. Missing IDs never imply deletion, and duplicate IDs in the uploaded data block the entire plan.</p>
<p>Choose these policies with the person responsible for the data. In particular, do not silently use “last row wins” when two uploaded rows disagree. That converts a validation problem into an order-dependent decision.</p>
<h2>Build and test the planner</h2>
<p>The code below expects JSON-derived records and a trusted current snapshot. It does not defend against executable getters or other arbitrary JavaScript objects. A <code>Map</code> provides the explicit ID lookup; its key-value behavior is defined in the <a href="https://tc39.es/ecma262/multipage/keyed-collections.html#sec-map-objects">ECMAScript specification</a>.</p>
<p>Save the whole block as <code>import-plan.test.mjs</code> and run <code>node import-plan.test.mjs</code>. No third-party packages are required.</p>
<pre><code class="language-javascript">import assert from 'node:assert/strict';

const valid = row =&gt; row !== null &amp;&amp; typeof row === 'object'
  &amp;&amp; Object.keys(row).sort().join(',') === 'id,name'
  &amp;&amp; typeof row.id === 'string' &amp;&amp; /^[A-Z][0-9]{2}$/.test(row.id)
  &amp;&amp; typeof row.name === 'string' &amp;&amp; row.name.trim().length &gt; 0
  &amp;&amp; row.name.length &lt;= 100;

export function buildPlan(current, incoming) {
  if (![current, incoming].every(a =&gt; Array.isArray(a) &amp;&amp; a.length &lt;= 1000))
    throw new TypeError('Expected bounded arrays');
  const existing = new Map();
  for (const row of current) {
    if (!valid(row) || existing.has(row.id))
      throw new TypeError('Invalid current snapshot');
    existing.set(row.id, row.name);
  }
  const seen = new Set(), errors = [], actions = [];
  for (const [index, row] of incoming.entries()) {
    if (!valid(row)) {
      errors.push({row: index + 1, reason: 'Invalid schema'});
      continue;
    }
    if (seen.has(row.id)) {
      errors.push({row: index + 1, reason: 'Duplicate id'});
      continue;
    }
    seen.add(row.id);
    const before = existing.get(row.id);
    if (before !== row.name) actions.push({
      kind: existing.has(row.id) ? 'update' : 'create',
      id: row.id, before: before ?? null, after: row.name,
    });
  }
  return {ready: errors.length === 0, errors,
    actions: errors.length ? [] : actions};
}

const current = [{id: 'A01', name: 'Draft'}, {id: 'B02', name: 'Keep'}];
const incoming = [{id: 'A01', name: 'Ready'}, {id: 'C03', name: 'New'},
  {id: 'B02', name: 'Keep'}];
const snapshot = structuredClone({current, incoming});
const plan = buildPlan(current, incoming);
assert.deepEqual(plan, {ready: true, errors: [], actions: [
  {kind: 'update', id: 'A01', before: 'Draft', after: 'Ready'},
  {kind: 'create', id: 'C03', before: null, after: 'New'},
]});
assert.deepEqual({current, incoming}, snapshot);
assert.deepEqual(buildPlan(current, incoming), plan);
assert.deepEqual(buildPlan(current, []), {ready: true, errors: [], actions: []});
for (const bad of [null, {id: 'D04', name: ''}, {id: 4, name: 'Bad'},
  {id: 'D04', name: 'Okay', extra: true}]) {
  const blocked = buildPlan(current, [...incoming, bad]);
  assert.equal(blocked.ready, false);
  assert.deepEqual(blocked.actions, []);
  assert.equal(blocked.errors[0].row, 4);
}
assert.equal(buildPlan(current, [...incoming, incoming[0]]).errors[0].reason,
  'Duplicate id');
assert.throws(() =&gt; buildPlan([...current, current[0]], []), TypeError);
assert.throws(() =&gt; buildPlan(current, Array(1001).fill(incoming[0])), TypeError);
console.log('Import-plan acceptance checks passed');
</code></pre>
<p>This exact file ran on Node.js v24.19.0 and printed <code>Import-plan acceptance checks passed</code>. The assertions check proposed creations and updates, unchanged inputs, repeatable results, empty imports, malformed records, duplicate IDs, and the row limit.</p>
<h2>Make the preview actionable</h2>
<p>The plan includes old and proposed values, so a reviewer can understand each change. Error positions are one-based array rows, not physical CSV line numbers. A quoted multiline CSV field can occupy several physical lines, so keep parser location information separately if your interface needs it.</p>
<p>A single invalid incoming row removes every executable action from the returned plan. This all-or-nothing preview policy prevents callers from accidentally applying the valid subset while ignoring errors. The planner also refuses an invalid current snapshot rather than making proposals from an ambiguous baseline.</p>
<p>For an actual client dataset, keep previews access-controlled and avoid writing complete rows into logs. Display only the fields needed for review. The names in this tutorial are short labels, not real client identities.</p>
<h2>Keep confirmation and execution outside this function</h2>
<p>A valid plan is still only a proposal. After explicit confirmation, the application must recheck permissions and the underlying data, and apply only the reviewed input. Keep the reviewed input server-side; do not trust a browser-supplied list of arbitrary write operations.</p>
<p>If another process changes the data after preview, reject the stale plan and request a fresh review. The actual executor needs transactional writes and appropriate concurrency checks. <a href="https://www.postgresql.org/docs/current/tutorial-transactions.html">PostgreSQL’s transaction documentation</a> explains atomic commit and rollback; its <a href="https://www.postgresql.org/docs/current/transaction-iso.html">isolation documentation</a> explains why concurrent changes need separate attention.</p>
<p>Keep notifications and other external effects out of preview entirely. A successful preview never authorizes those effects. Database rollback cannot retract an email already sent. The useful boundary is visible in the function signature: the preview receives values and produces values, while a separately authorized operation owns persistence.</p>
]]></content:encoded></item><item><title><![CDATA[Turn CSV Acceptance Criteria into Executable Tests]]></title><description><![CDATA[By Scopefolio Support. Prepared with AI assistance. “Export these records as CSV” leaves several decisions unresolved. Does an empty export contain a…]]></description><link>https://scopefolio.hashnode.dev/turn-csv-acceptance-criteria-into-executable-tests</link><guid isPermaLink="true">https://scopefolio.hashnode.dev/turn-csv-acceptance-criteria-into-executable-tests</guid><dc:creator><![CDATA[Scopefolio Support]]></dc:creator><pubDate>Sun, 11 Oct 2026 08:55:37 GMT</pubDate><content:encoded><![CDATA[<p>By Scopefolio Support. Prepared with AI assistance.</p>
<p>“Export these records as CSV” leaves several decisions unresolved. Does an empty export contain a header? Can a note contain a comma? What happens when a record has fewer fields than the header? A screenshot of a successful download answers none of those questions.</p>
<p>For a small client project, a useful next step is to turn each decision into an input and an exact expected result. The exercise below builds a bounded, in-memory exporter. It is an independent teaching example, with fictional data.</p>
<h2>Write the contract before the function</h2>
<p>This export has five acceptance criteria:</p>
<ol>
<li>Always include the supplied header, even when there are no data rows</li>
<li>End every record with carriage return plus line feed, written as <code>\r\n</code></li>
<li>Quote fields containing a comma, double quote, carriage return, or line feed; double every embedded quote</li>
<li>Preserve empty strings and spaces; reject missing cells and non-string values</li>
<li>Reject oversized input instead of silently truncating it</li>
</ol>
<p>The quoting and record-separator choices follow the conventions described in <a href="https://datatracker.ietf.org/doc/html/rfc4180#section-2">RFC 4180</a>. That document is informational, and CSV consumers vary. These criteria describe this exporter’s explicit contract rather than every application that opens CSV.</p>
<p>Notice that these rules apply to headers too. A header containing a comma needs the same escaping as a data field. Keeping one encoding path prevents the two from drifting apart.</p>
<h2>Run the implementation and its examples</h2>
<p>Save this complete file as <code>csv.test.mjs</code>, then run <code>node csv.test.mjs</code>. It uses Node’s built-in <a href="https://nodejs.org/api/assert.html#strict-assertion-mode">strict assertions</a>, so no package installation is needed.</p>
<pre><code class="language-javascript">import assert from 'node:assert/strict';

export function toCsv(headers, rows) {
  if (!Array.isArray(headers) || headers.length &lt; 1 || headers.length &gt; 20)
    throw new TypeError('Expected 1–20 headers');
  if (!Array.isArray(rows) || rows.length &gt; 1000)
    throw new TypeError('Expected at most 1000 rows');
  let result = '';
  for (const row of [headers, ...rows]) {
    if (!Array.isArray(row) || row.length !== headers.length)
      throw new TypeError('Wrong column count');
    const cells = Array.from(row, value =&gt; {
      if (typeof value !== 'string' || value.length &gt; 10000)
        throw new TypeError('Expected a short string');
      return /[",\r\n]/.test(value)
        ? `"${value.replaceAll('"', '""')}"` : value;
    });
    result += cells.join(',') + '\r\n';
    if (result.length &gt; 1000000) throw new RangeError('Export too large');
  }
  return result;
}

assert.equal(toCsv(['name', 'note'], []), 'name,note\r\n');
assert.equal(toCsv(['name', 'note'], [['Mina', 'ready']]),
  'name,note\r\nMina,ready\r\n');
for (const [input, encoded] of [
  ['a,b', '"a,b"'], ['a"b', '"a""b"'],
  ['a\nb', '"a\nb"'], ['a\rb', '"a\rb"'],
  ['a\r\nb', '"a\r\nb"'], ['', ''], [' x ', ' x '],
]) assert.equal(toCsv(['value'], [[input]]), `value\r\n${encoded}\r\n`);
assert.equal(toCsv(['a,b'], [['ok']]), '"a,b"\r\nok\r\n');
for (const rows of [[['too', 'wide']], [[null]], [Array(1)]])
  assert.throws(() =&gt; toCsv(['value'], rows), TypeError);
assert.throws(() =&gt; toCsv([], []), TypeError);
assert.throws(() =&gt; toCsv(['v'], Array(1001).fill(['x'])), TypeError);
assert.throws(() =&gt; toCsv(['v'], [['x'.repeat(10001)]]), TypeError);
assert.throws(() =&gt; toCsv(['v'], Array(101).fill(['x'.repeat(10000)])), RangeError);
assert.equal(toCsv(['value'], [['=1+1']]), 'value\r\n=1+1\r\n');
console.log('CSV acceptance checks passed');
</code></pre>
<p>The file was run with Node.js v24.19.0 and printed <code>CSV acceptance checks passed</code>. That result covers these fixtures; it does not establish compatibility with every spreadsheet or downstream importer.</p>
<h2>Make failures explain the requirement</h2>
<p>The expected strings are written independently of the encoder. Constructing expected values by calling the same escaping helper would allow a shared mistake to pass. The quote fixture distinguishes doubling an embedded quote from merely wrapping the cell. Separate carriage-return and line-feed fixtures catch a common incomplete check that only looks for <code>\n</code>.</p>
<p>An export containing the right number of lines can still corrupt a quoted newline, so compare complete records rather than splitting on newlines.</p>
<p>The sparse-array case is deliberate. JavaScript arrays can have a length without a value at each position. <code>Array.from</code> visits those positions and passes <code>undefined</code> to validation, preventing an accidental empty-field conversion.</p>
<p>The limits are teaching defaults: 20 columns, 1,000 data rows, 10,000 UTF-16 code units per cell, and 1,000,000 code units in the output. They are not UTF-8 byte limits or measured capacity recommendations. This function holds the output in memory; a large production export needs a streaming design and separately tested backpressure and cancellation behavior.</p>
<h2>Keep format correctness separate from product decisions</h2>
<p>The last assertion preserves <code>=1+1</code> exactly. That is important evidence about the contract: CSV quoting alone does not prevent spreadsheet formula interpretation. If users will open untrusted values in a spreadsheet, agree on a separate safety policy and test it against the actual consumer. <a href="https://community.owasp.org/attacks/CSV_Injection">OWASP’s CSV injection guidance</a> describes this risk and cautions that mitigations vary across spreadsheet workflows.</p>
<p>Also decide how your application selects records, checks export permissions, formats dates, encodes the returned string, and names the download. Those belong in additional tests at their own boundaries. This example deliberately accepts already formatted strings and performs no file or network writes.</p>
<p>Acceptance criteria become useful when a reader can point to a failing example and understand which promise was broken. Start with that mapping, then expand the test set whenever a real input exposes another decision.</p>
]]></content:encoded></item><item><title><![CDATA[Make requirement change summaries readable with a semantic diff]]></title><description><![CDATA[By Scopefolio Support. Prepared with AI assistance. A JSON diff can tell you that an array changed on six lines. A developer reviewing a project update…]]></description><link>https://scopefolio.hashnode.dev/make-requirement-change-summaries-readable-with-a-semantic-diff</link><guid isPermaLink="true">https://scopefolio.hashnode.dev/make-requirement-change-summaries-readable-with-a-semantic-diff</guid><dc:creator><![CDATA[Scopefolio Support]]></dc:creator><pubDate>Sun, 11 Oct 2026 08:40:35 GMT</pubDate><content:encoded><![CDATA[<p>By Scopefolio Support. Prepared with AI assistance.</p>
<p>A JSON diff can tell you that an array changed on six lines. A developer reviewing a project update needs a more useful answer: which requirement was added, what changed in the export feature, and whether someone merely rearranged the list.</p>
<p>A small semantic diff can answer those questions without an AI service or a general-purpose diff dependency. The important work is choosing identity and comparison rules before writing the loop.</p>
<h2>Give each requirement an identity</h2>
<p>Use a stable ID that survives renaming and reordering. A title is editable content. An array position changes whenever someone inserts an item above it. Neither makes a reliable identity.</p>
<p>This example compares three fields: title, acceptance text, and estimated whole hours. Extra metadata is ignored deliberately. Duplicate IDs are rejected because silently overwriting a record would hide a change.</p>
<p>A JavaScript <code>Map</code> makes those ID lookups explicit; its keys are unique, as described in the <a href="https://tc39.es/ecma262/multipage/keyed-collections.html#sec-map-objects">ECMAScript Map specification</a>. Save this as <code>diff.mjs</code>:</p>
<pre><code class="language-javascript">export function index(rows) {
  if (!Array.isArray(rows)) throw new Error("Expected an array");
  const map = new Map();
  for (const row of rows) {
    if (!row || typeof row.id !== "string" || !row.id.trim() ||
        typeof row.title !== "string" ||
        typeof row.acceptance !== "string" ||
        !Number.isSafeInteger(row.hours) || row.hours &lt; 0) {
      throw new Error("Invalid requirement");
    }
    if (map.has(row.id)) throw new Error(`Duplicate ID: ${row.id}`);
    map.set(row.id, row);
  }
  return map;
}

export function diff(before, after) {
  const old = index(before), next = index(after);
  const added = after.filter(row =&gt; !old.has(row.id));
  const removed = before.filter(row =&gt; !next.has(row.id));
  const modified = [];
  for (const row of after) {
    const previous = old.get(row.id);
    if (!previous) continue;
    const changes = [];
    for (const field of ["title", "acceptance", "hours"]) {
      if (previous[field] !== row[field]) {
        changes.push({field, before: previous[field], after: row[field]});
      }
    }
    if (changes.length) modified.push({id: row.id, changes});
  }
  const oldOrder = before.filter(row =&gt; next.has(row.id)).map(row =&gt; row.id);
  const newOrder = after.filter(row =&gt; old.has(row.id)).map(row =&gt; row.id);
  const reordered = oldOrder.some((id, i) =&gt; id !== newOrder[i]);
  return {added, removed, modified, reordered};
}
</code></pre>
<h2>Compare the surviving sequence</h2>
<p>The order check first removes IDs absent from the other snapshot. That detail prevents a misleading result: inserting a new requirement at the top shifts every numeric position, but the existing requirements still have the same relative order.</p>
<p>Reversing two surviving IDs sets <code>reordered</code> to true. Their content stays out of <code>modified</code> unless a compared field also changed. A single update can therefore contain both content edits and a reorder.</p>
<p>String comparison is exact. Changing punctuation or whitespace in acceptance text appears as an edit. If your domain treats certain formatting as equivalent, define that normalization at the input boundary and preserve the original text for display. Automatically lowercasing requirements could conceal meaningful changes in identifiers or examples.</p>
<h2>Check the cases that confuse line diffs</h2>
<p>Save this beside the implementation as <code>diff.test.mjs</code>:</p>
<pre><code class="language-javascript">import assert from "node:assert/strict";
import test from "node:test";
import {diff} from "./diff.mjs";

const a = {id: "export", title: "Export", acceptance: "CSV download", hours: 4};
const b = {id: "search", title: "Search", acceptance: "Match titles", hours: 3};
const c = {id: "sort", title: "Sort", acceptance: "Newest first", hours: 2};
const empty = {added: [], removed: [], modified: [], reordered: false};

test("identity and property order do not create changes", () =&gt; {
  assert.deepEqual(diff([a], [{hours: 4, acceptance: a.acceptance,
    title: a.title, id: a.id}]), empty);
});
test("addition and removal retain their records", () =&gt; {
  assert.deepEqual(diff([a, b], [a, c]), {...empty, added: [c], removed: [b]});
});
test("rename and estimate change stay attached to the same ID", () =&gt; {
  assert.deepEqual(diff([a], [{...a, title: "Export CSV", hours: 6}]).modified,
    [{id: "export", changes: [
      {field: "title", before: "Export", after: "Export CSV"},
      {field: "hours", before: 4, after: 6}
    ]}]);
});
test("reordering leaves content unchanged", () =&gt; {
  assert.deepEqual(diff([a, b], [b, a]), {...empty, reordered: true});
});
test("insertion does not move surviving requirements", () =&gt; {
  assert.equal(diff([a, b], [c, a, b]).reordered, false);
});
test("duplicates and invalid estimates fail explicitly", () =&gt; {
  assert.throws(() =&gt; diff([a, a], [a]), /Duplicate ID/);
  assert.throws(() =&gt; diff([a], [a, a]), /Duplicate ID/);
  assert.throws(() =&gt; diff([a], [{...a, hours: NaN}]), /Invalid/);
});
</code></pre>
<p>Run <code>node --test diff.test.mjs</code>. These exact files were executed on Node v24.19.0: six tests passed, with no failures or skips. The <a href="https://nodejs.org/api/test.html">Node test runner documentation</a> covers the built-in runner.</p>
<h2>Turn the result into a useful summary</h2>
<p>Render modifications by ID and field: “Export: estimated hours changed from 4 to 6.” Keep additions and removals separate. Show a short ordering notice when <code>reordered</code> is true; the boolean does not identify a minimal set of moves.</p>
<p>Keep both source snapshots alongside the result. This function returns references for added and removed records, so clone them if the caller will mutate the snapshots. When displaying text in HTML, use text nodes or your framework's normal escaping.</p>
<p>Finally, keep identity stable across storage and editing. Regenerating every ID on save makes every requirement look newly created. A readable summary begins with a data model that can recognize the same requirement tomorrow.</p>
]]></content:encoded></item><item><title><![CDATA[How to calculate revised delivery dates without counting weekends twice]]></title><description><![CDATA[By Scopefolio Support. Prepared with AI assistance. A client adds an export feature to a software project due on Friday, November 13, 2026. The estimated…]]></description><link>https://scopefolio.hashnode.dev/how-to-calculate-revised-delivery-dates-without-counting-weekends-twice</link><guid isPermaLink="true">https://scopefolio.hashnode.dev/how-to-calculate-revised-delivery-dates-without-counting-weekends-twice</guid><dc:creator><![CDATA[Scopefolio Support]]></dc:creator><pubDate>Sun, 11 Oct 2026 08:06:08 GMT</pubDate><content:encoded><![CDATA[<p>By Scopefolio Support. Prepared with AI assistance.</p>
<p>A client adds an export feature to a software project due on Friday, November 13, 2026. The estimated schedule impact is two working days. With a Monday–Friday calendar, the proposed delivery date becomes Tuesday, November 17.</p>
<p>Two bugs can quietly move that date again. A calculator might add a weekend adjustment after it has already skipped weekends. Or an edited scope request might add its entire estimate to an already revised deadline.</p>
<p>The following standalone JavaScript example prevents both: visit each calendar date once, and recalculate from the unchanged original deadline using the latest total schedule impact.</p>
<h2>Define a working day</h2>
<p>This example uses a deliberately small policy:</p>
<ul>
<li>Saturday and Sunday are nonworking days</li>
<li>Counting starts the day after the original deadline</li>
<li>Zero extra days leaves the original date unchanged, even on a weekend</li>
<li>Explicit holiday dates are also excluded</li>
<li>Inputs are whole working days, from zero through 3,660</li>
</ul>
<p>The holiday list belongs to the project calendar. November 16 is a fictional team closure in the example below, with no claim about any country's public holidays. Weekend holidays receive no additional adjustment. Observed holidays must be supplied explicitly.</p>
<p>First decide the schedule impact of a scope change. Two person-days of effort do not necessarily move delivery by two days: parallel work, dependencies, and availability can change the result. The function accepts an already assessed delay in working days.</p>
<h2>Keep calendar arithmetic independent of local time</h2>
<p>Store deadlines as strict <code>YYYY-MM-DD</code> strings. Internally, use UTC fields consistently, keeping local timezone offsets and daylight-saving transitions out of the calculation. UTC is an implementation convention here; the deadline remains a calendar date.</p>
<p>Save this as <code>delivery-date.mjs</code>:</p>
<pre><code class="language-js">function parseDateOnly(value) {
  if (typeof value !== 'string' || value.length !== 10 ||
      !/^\d{4}-\d{2}-\d{2}$/.test(value)) {
    throw new TypeError('Expected YYYY-MM-DD');
  }
  const [year, month, day] = value.split('-').map(Number);
  const date = new Date(0);
  date.setUTCFullYear(year, month - 1, day);
  if (year &lt; 1 || date.getUTCFullYear() !== year ||
      date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
    throw new RangeError('Invalid calendar date');
  }
  return date;
}

export function revisedDeliveryDate(base, totalDays, holidays = new Set()) {
  const date = parseDateOnly(base);
  if (!Number.isInteger(totalDays) || totalDays &lt; 0 || totalDays &gt; 3660) {
    throw new RangeError('Expected 0 to 3660 whole working days');
  }
  if (!(holidays instanceof Set) || holidays.size &gt; 1000) {
    throw new TypeError('Expected a Set of at most 1000 holiday dates');
  }
  for (const holiday of holidays) parseDateOnly(holiday);
  let remaining = totalDays;
  while (remaining &gt; 0) {
    date.setUTCDate(date.getUTCDate() + 1);
    if (date.getUTCFullYear() &gt; 9999) {
      throw new RangeError('Result exceeds year 9999');
    }
    const weekday = date.getUTCDay();
    const key = date.toISOString().slice(0, 10);
    if (weekday !== 0 &amp;&amp; weekday !== 6 &amp;&amp; !holidays.has(key)) remaining--;
  }
  return date.toISOString().slice(0, 10);
}
</code></pre>
<p>JavaScript's date setters normalize overflowing fields, so the parser checks the resulting year, month, and day against the supplied values. That rejects impossible dates such as <code>2026-02-29</code>. The exact-length check also rejects trailing newlines. <a href="https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-date.prototype.setutcfullyear">ECMAScript specifies the UTC year setter</a>.</p>
<p>Inside the loop, one calendar day is visited per iteration. The remaining count decreases only when that date is both a weekday and absent from the holiday set. There is no separate weekend-padding pass. <a href="https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-date.prototype.setutcdate">The UTC date setter defines the calendar rollover behavior</a>.</p>
<p>The 3,660-day and 1,000-holiday limits are example application bounds. The supported year range is 0001–9999; overflow is rejected. Every holiday is validated, including when the requested offset is zero.</p>
<h2>Recalculate from the original deadline</h2>
<p>Save this as <code>example.mjs</code> beside the function:</p>
<pre><code class="language-js">import assert from 'node:assert/strict';
import { revisedDeliveryDate as revise } from './delivery-date.mjs';

const original = '2026-11-13';
assert.equal(revise(original, 2), '2026-11-17');
assert.equal(revise(original, 2), '2026-11-17'); // Recalculate safely.
assert.equal(revise(original, 3), '2026-11-18'); // New total, same base.
assert.equal(original, '2026-11-13');
const holidays = new Set(['2026-11-16']); // Fictional team closure.
assert.equal(revise(original, 2, holidays), '2026-11-18');
console.log('Examples passed');
</code></pre>
<p>Run <code>node example.mjs</code>; successful execution prints <code>Examples passed</code>.</p>
<p>The second call deliberately repeats the first. Both return November 17 because the inputs are identical. When the total grows from two to three working days, the proposal becomes November 18. Passing the previous proposal back as the base would apply the earlier delay again. In this example, adding two working days to November 17 produces November 19, even though the scope estimate has not changed.</p>
<p>Keep the original deadline, the latest total schedule impact, and the proposed date as separate values. When an estimate decreases or a request is removed, recompute from the same original deadline. Also preserve the calendar used for the proposal: changing holiday inputs can legitimately change the result.</p>
<p>At the storage boundary, keep the date-only string intact. A form should display that calendar date without first converting it into a local timestamp. Store review timestamps separately when you need to know precisely when a proposal was created or approved. These values answer different questions and need different validation rules.</p>
<h2>Test before using the proposal</h2>
<p>This exact implementation and a 15-test companion suite were executed on Node v24.19.0 under UTC, America/New_York, Europe/London, Asia/Kolkata, and Pacific/Auckland. All 15 tests passed in each timezone, with zero failures or skips.</p>
<p>The suite covers repeated calculations, weekend/holiday overlap, zero offsets, leap years, year boundaries, daylight-saving transition weekends, invalid inputs, and the supported bounds. Run equivalent cases in your target runtime and project calendar.</p>
<p>Finally, show the calculation alongside the scope revision: original deadline, total additional working days, calendar assumptions, and proposed date. Keep client approval as a separate recorded decision. A deterministic calculation makes a proposal reproducible; it does not establish agreement to a new delivery commitment.</p>
]]></content:encoded></item><item><title><![CDATA[Website handover checklist: what clients should approve before launch]]></title><description><![CDATA[A fixed-price website can be ready to deploy while the handover is still unclear. The client has seen the homepage, but who approves the final content? Who…]]></description><link>https://scopefolio.hashnode.dev/website-handover-checklist-what-clients-should-approve-before-launch</link><guid isPermaLink="true">https://scopefolio.hashnode.dev/website-handover-checklist-what-clients-should-approve-before-launch</guid><dc:creator><![CDATA[Scopefolio Support]]></dc:creator><pubDate>Wed, 07 Oct 2026 11:19:29 GMT</pubDate><content:encoded><![CDATA[<p>A fixed-price website can be ready to deploy while the handover is still unclear. The client has seen the homepage, but who approves the final content? Who owns the hosting account? What happens when someone finds a problem next week?</p>
<p>A short delivery review helps you answer those questions before launch. Start with the agreed scope and approved changes, then give the client one review link, one checklist and a clear decision to make.</p>
<h2>1. Identify the exact version being reviewed</h2>
<p>Label the delivery with a date or version and include its staging link. List the pages, features and approved changes included in that version.</p>
<p>Keep a dated record of what the client reviewed. If you change something material afterward, identify the change and ask for the relevant checks again. An approval attached to an old preview leaves everyone guessing.</p>
<h2>2. Check the agreed customer journeys</h2>
<p>Ask the client to work through the journeys that matter to this website:</p>
<ul>
<li>Find the service, product or information a visitor needs.</li>
<li>Use the navigation and agreed mobile layouts.</li>
<li>Submit a test enquiry and confirm it reaches the intended inbox.</li>
<li>Complete any included booking, purchase or account flow in the appropriate test environment.</li>
<li>Check page copy, contact details, images and download links.</li>
</ul>
<p>Choose checks that match your agreed deliverables. Share your own test results alongside the review request so the client has evidence to work from.</p>
<h2>3. Record remaining issues before requesting approval</h2>
<p>Make one short list containing each issue, its owner and the agreed next step. Mark anything that prevents launch. For smaller items, get an explicit decision about whether they can follow after launch and when they are due.</p>
<p>Keep feedback specific. A form that fails an agreed acceptance check is a defect to assess against the agreement. A newly requested customer portal is a scope change. Clarifications and included revisions may need their own treatment.</p>
<p>Resolve that classification before promising a fix date or quoting extra work. For the longer process, see <a href="https://scopefolio.hashnode.dev/how-to-handle-scope-changes-in-fixed-price-web-projects">our guide to handling scope changes in fixed-price web projects</a>.</p>
<h2>4. Confirm ownership and the handover plan</h2>
<p>List each service or asset the client needs to operate the site:</p>
<ul>
<li>Domain registration, DNS and hosting</li>
<li>CMS, repository and deployment access, where included</li>
<li>Analytics, enquiry forms and other connected services</li>
<li>Source files, content assets and applicable licence information</li>
<li>Setup notes and any agreed training or walkthrough</li>
</ul>
<p>For each item, record the owner, billing responsibility and how access will be provided. Use named accounts and provider invitations where available. If credentials must be transferred, use an agreed password-manager sharing method rather than ordinary email or chat.</p>
<p>Confirm the client can actually sign in and perform the agreed tasks at handover. Record which freelancer access remains necessary for support.</p>
<h2>5. Agree on launch and aftercare</h2>
<p>Name the person who can approve the delivery and authorise launch. Confirm the launch window, who performs it, the immediate live-site checks and who handles problems.</p>
<p>Write down the support period, reporting channel, included coverage and who owns ongoing updates, backups and renewals. Use specific dates rather than “support after launch.”</p>
<h2>Copyable delivery-approval message</h2>
<p>Hi [name],</p>
<p>The [version/date] website delivery is ready to review: [staging link].</p>
<p>Please check [acceptance checklist] and review [handover plan]. Open items and their proposed next steps are here: [list].</p>
<p>By [review date], please reply with either:</p>
<ul>
<li>Approval of this delivery, including any explicitly agreed open items; or</li>
<li>The page/feature, expected result and actual result for anything needing attention.</li>
</ul>
<p>Please also confirm whether you authorise launch during [window]. We’ll confirm the handover and live-site checks separately.</p>
<p>The agreed support period is [dates], covering [coverage]. Report issues through [channel].</p>
<p>Thanks,<br />[name]</p>
<p>Keep this review with the project’s other decisions so everyone can find what was accepted and what still needs work.</p>
<p>This article is from Scopefolio Support, with AI assistance. Scopefolio offers free early access for recording scope, client approvals, deliveries and handover responsibilities: <a href="https://scopefolio.neatgrove.com/">https://scopefolio.neatgrove.com/</a>.</p>
]]></content:encoded></item><item><title><![CDATA[How to handle scope changes in fixed price web projects]]></title><description><![CDATA[By Scopefolio Support Drafted with AI assistance Scopefolio is our product. A client asks for “one small change.” Before estimating it, work out which…]]></description><link>https://scopefolio.hashnode.dev/how-to-handle-scope-changes-in-fixed-price-web-projects</link><guid isPermaLink="true">https://scopefolio.hashnode.dev/how-to-handle-scope-changes-in-fixed-price-web-projects</guid><dc:creator><![CDATA[Scopefolio Support]]></dc:creator><pubDate>Fri, 02 Oct 2026 12:40:16 GMT</pubDate><content:encoded><![CDATA[<p>By Scopefolio Support</p>
<p>Drafted with AI assistance</p>
<p>Scopefolio is our product.</p>
<p>A client asks for “one small change.” Before estimating it, work out which promise the request touches. You may owe a correction, have room for an included revision, or need a new agreement. Classifying the request fairly gives a freelance web developer a better starting point than its apparent size. Use the process below to protect the agreed delivery while giving the client a clear choice.</p>
<h2>Compare the request with the agreement</h2>
<p>Use three working categories, based on the scope and review terms you actually agreed:</p>
<ul>
<li><p>A defect is work that fails an agreed requirement. If the gallery was meant to show complete images but crops them, investigate and correct that failure under the existing agreement. A missed requirement does not become extra work because fixing it takes longer than expected.</p>
</li>
<li><p>An included revision changes an existing deliverable within the agreed review allowance. Reordering supplied gallery images might qualify. Define what a round covers and how feedback is collected; “one round” should not leave either side guessing about its limits.</p>
</li>
<li><p>A scope change alters the agreed result, quantity or process. Adding gallery filters may introduce new behaviour. An additional review round can also change the allowance, even if each individual edit seems small.</p>
</li>
</ul>
<p>Split mixed requests into separate items. A client can report a defect and request a feature in the same sentence. Reaching the revision limit should not turn a genuine defect into a paid revision.</p>
<h2>Give completion an observable test</h2>
<p>Write acceptance criteria before work starts. For a gallery, specify the image count, expected display, interactions and agreed browser checks. Name who supplies the images and whether image editing is included. Record the design version both sides are approving.</p>
<p>Make each check something you can demonstrate together. “Looks professional” invites different interpretations; “displays the twelve supplied images without cropping at the agreed mobile and desktop sizes” gives you a test. Acceptance checks should sit alongside the price, delivery date, client dependencies and review allowance.</p>
<h2>Resolve gaps without inventing certainty</h2>
<p>Sometimes the brief says only “add a gallery.” A missing detail alone does not establish that a request is chargeable. Review the proposal, approved designs and relevant messages together. Ask what the client expected and explain your interpretation. Then agree the missing detail and its impact before continuing the disputed part.</p>
<p>If you choose a goodwill exception, record exactly what you will include and whether it affects delivery. Do not let an unrecorded favour quietly expand every future request. For an uncertain integration, propose a bounded investigation before promising a fixed implementation price.</p>
<h2>Show the whole impact before requesting approval</h2>
<p>A useful change proposal identifies the current approved version, the requested result and its new acceptance checks. Estimate implementation, testing and deployment work, and identify any client inputs. Show the price adjustment, complete revised total and revised delivery date. Offer a smaller version, a later phase or the original scope when those are realistic options.</p>
<p>Here is a fictional example. The amounts illustrate the decision, not a recommended rate. A four-page portfolio costs GBP 1,800 and is due on 22 October. It includes a twelve-image gallery without cropping and one consolidated feedback round. During that round, the client requests three things:</p>
<ul>
<li><p>Fix cropped images: a defect against the agreed display requirement</p>
</li>
<li><p>Reorder the images: an included revision</p>
</li>
<li><p>Add filters by service: a proposed scope change</p>
</li>
</ul>
<p>The filter proposal adds GBP 240, making the total GBP 2,040, and moves delivery to 26 October. It covers three service filters plus an “All” view, with the selected filter showing only matching images. Delivery assumes approval by 12 October and labels supplied by 14 October. The original fixes and reordering remain covered.</p>
<h2>Ask for a decision the client can answer</h2>
<p>Copy and adapt this message after checking the request against your agreement. Separate defects and included revisions from proposed changes. If the scope is unclear, resolve that first.</p>
<p>Subject: [Project] change proposal [version]</p>
<p>Hi [client approver],</p>
<p>I’ve checked your request for [request] against our approved scope [version, date and link].</p>
<p>Already covered: [defects or included revisions to handle under the current agreement]</p>
<p>Proposed change: [what will be added, removed or changed]</p>
<p>Reason: [the agreed deliverable, quantity or review limit this changes]</p>
<p>Completion check: [how we’ll verify the changed work]</p>
<p>Price: [currency and current total], with a [currency and increase/decrease] adjustment. The revised project total is [currency and new total].</p>
<p>Delivery: [current date] becomes [proposed date].</p>
<p>Dependencies: approval by [date], plus [client inputs, responsible person and due date].</p>
<p>Please reply “I approve [project] change proposal [exact version], including its scope, revised total, delivery date and dependencies,” or tell me what you’d like adjusted.</p>
<p>I’ll keep the proposed change pending until you approve it. [Explain any effect on the existing work while the decision is pending.] If approval or inputs arrive late, I’ll confirm the delivery date again.</p>
<p>Send the proposal to the designated approver. Save their explicit decision with the exact version. An opened link or unanswered message is not approval. If the scope, price or date changes during discussion, update the proposal and request a decision on that version. Reconfirm delivery if approval or client inputs arrive late.</p>
<h2>Check each request before starting</h2>
<ul>
<li><p>Which agreed requirement or allowance does this request touch?</p>
</li>
<li><p>Have defects, included revisions and new work been separated?</p>
</li>
<li><p>Can both sides test the proposed result?</p>
</li>
<li><p>Are the revised total, delivery date and dependencies clear?</p>
</li>
<li><p>If the agreement changes, has the designated approver approved this exact version?</p>
</li>
</ul>
<p>Keep that record with the project so the next request starts from the same agreement.</p>
<p>You can use this workflow in a shared document or <a href="https://scopefolio.neatgrove.com/">explore Scopefolio free early access</a>, with no card required.</p>
]]></content:encoded></item></channel></rss>