{
  "format": "debug-date-lab-v1",
  "files": {
    "baseline/date.mjs": "const pad = value => String(value).padStart(2, '0');\n\nfunction requireCivilDate(value) {\n  if (typeof value !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) {\n    throw new RangeError('Expected a civil date in YYYY-MM-DD format');\n  }\n  const parsed = new Date(`${value}T00:00:00.000Z`);\n  if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value) {\n    throw new RangeError('Invalid calendar date');\n  }\n}\n\nexport function formatDueDate(value) {\n  requireCivilDate(value);\n  const date = new Date(value);\n  return `${pad(date.getDate())}/${pad(date.getMonth() + 1)}/${date.getFullYear()}`;\n}\n\nexport function formatInstant(value, timeZone) {\n  return new Intl.DateTimeFormat('en-GB', {\n    day: '2-digit', month: '2-digit', year: 'numeric', timeZone,\n  }).format(new Date(value));\n}\n",
    "baseline/probe.mjs": "import { formatDueDate, formatInstant } from './date.mjs';\n\nconst input = '2026-10-25';\nconst parsed = new Date(input);\nconsole.log(JSON.stringify({\n  requestedZone: process.env.TZ,\n  resolvedZone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n  input,\n  epochMs: parsed.getTime(),\n  iso: parsed.toISOString(),\n  local: { year: parsed.getFullYear(), month: parsed.getMonth() + 1, day: parsed.getDate() },\n  dueDate: formatDueDate(input),\n  instant: formatInstant('2026-10-25T00:30:00.000Z'),\n}, null, 2));\n",
    "baseline/date.test.mjs": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { formatDueDate, formatInstant } from './date.mjs';\n\ntest('civil due date keeps its calendar day', () => {\n  assert.equal(formatDueDate('2026-10-25'), '25/10/2026');\n});\ntest('civil date on the spring clock change keeps its calendar day', () => {\n  assert.equal(formatDueDate('2026-03-29'), '29/03/2026');\n});\ntest('valid leap day is accepted', () => {\n  assert.equal(formatDueDate('2024-02-29'), '29/02/2024');\n});\ntest('non-leap February 29 is rejected', () => {\n  assert.throws(() => formatDueDate('2026-02-29'), RangeError);\n});\ntest('February 30 is rejected', () => {\n  assert.throws(() => formatDueDate('2026-02-30'), RangeError);\n});\ntest('month 13 is rejected', () => {\n  assert.throws(() => formatDueDate('2026-13-01'), RangeError);\n});\ntest('timestamp is rejected by the civil-date function', () => {\n  assert.throws(() => formatDueDate('2026-10-25T00:30:00Z'), RangeError);\n});\ntest('noncanonical date is rejected', () => {\n  assert.throws(() => formatDueDate('2026-1-5'), RangeError);\n});\ntest('a real instant has October 25 in UTC', () => {\n  assert.equal(formatInstant('2026-10-25T00:30:00Z', 'UTC'), '25/10/2026');\n});\ntest('the same real instant has October 24 in New York', () => {\n  assert.equal(formatInstant('2026-10-25T00:30:00Z', 'America/New_York'), '24/10/2026');\n});\ntest('the default instant display keeps the configured application timezone', () => {\n  assert.ok(process.env.EXPECTED_INSTANT_DATE, 'Set EXPECTED_INSTANT_DATE for this run');\n  assert.equal(formatInstant('2026-10-25T00:30:00Z'), process.env.EXPECTED_INSTANT_DATE);\n});\n",
    "fixed/date.mjs": "function requireCivilDate(value) {\n  if (typeof value !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) {\n    throw new RangeError('Expected a civil date in YYYY-MM-DD format');\n  }\n  const parsed = new Date(`${value}T00:00:00.000Z`);\n  if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value) {\n    throw new RangeError('Invalid calendar date');\n  }\n}\n\nexport function formatDueDate(value) {\n  requireCivilDate(value);\n  const [year, month, day] = value.split('-');\n  return `${day}/${month}/${year}`;\n}\n\nexport function formatInstant(value, timeZone) {\n  return new Intl.DateTimeFormat('en-GB', {\n    day: '2-digit', month: '2-digit', year: 'numeric', timeZone,\n  }).format(new Date(value));\n}\n",
    "fixed/probe.mjs": "import { formatDueDate, formatInstant } from './date.mjs';\n\nconst input = '2026-10-25';\nconst parsed = new Date(input);\nconsole.log(JSON.stringify({\n  requestedZone: process.env.TZ,\n  resolvedZone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n  input,\n  epochMs: parsed.getTime(),\n  iso: parsed.toISOString(),\n  local: { year: parsed.getFullYear(), month: parsed.getMonth() + 1, day: parsed.getDate() },\n  dueDate: formatDueDate(input),\n  instant: formatInstant('2026-10-25T00:30:00.000Z'),\n}, null, 2));\n",
    "fixed/date.test.mjs": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { formatDueDate, formatInstant } from './date.mjs';\n\ntest('civil due date keeps its calendar day', () => {\n  assert.equal(formatDueDate('2026-10-25'), '25/10/2026');\n});\ntest('civil date on the spring clock change keeps its calendar day', () => {\n  assert.equal(formatDueDate('2026-03-29'), '29/03/2026');\n});\ntest('valid leap day is accepted', () => {\n  assert.equal(formatDueDate('2024-02-29'), '29/02/2024');\n});\ntest('non-leap February 29 is rejected', () => {\n  assert.throws(() => formatDueDate('2026-02-29'), RangeError);\n});\ntest('February 30 is rejected', () => {\n  assert.throws(() => formatDueDate('2026-02-30'), RangeError);\n});\ntest('month 13 is rejected', () => {\n  assert.throws(() => formatDueDate('2026-13-01'), RangeError);\n});\ntest('timestamp is rejected by the civil-date function', () => {\n  assert.throws(() => formatDueDate('2026-10-25T00:30:00Z'), RangeError);\n});\ntest('noncanonical date is rejected', () => {\n  assert.throws(() => formatDueDate('2026-1-5'), RangeError);\n});\ntest('a real instant has October 25 in UTC', () => {\n  assert.equal(formatInstant('2026-10-25T00:30:00Z', 'UTC'), '25/10/2026');\n});\ntest('the same real instant has October 24 in New York', () => {\n  assert.equal(formatInstant('2026-10-25T00:30:00Z', 'America/New_York'), '24/10/2026');\n});\ntest('the default instant display keeps the configured application timezone', () => {\n  assert.ok(process.env.EXPECTED_INSTANT_DATE, 'Set EXPECTED_INSTANT_DATE for this run');\n  assert.equal(formatInstant('2026-10-25T00:30:00Z'), process.env.EXPECTED_INSTANT_DATE);\n});\n",
    "wrong-global-utc/date.mjs": "process.env.TZ = 'UTC';\n\nconst pad = value => String(value).padStart(2, '0');\n\nfunction requireCivilDate(value) {\n  if (typeof value !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) {\n    throw new RangeError('Expected a civil date in YYYY-MM-DD format');\n  }\n  const parsed = new Date(`${value}T00:00:00.000Z`);\n  if (!Number.isFinite(parsed.getTime()) || parsed.toISOString().slice(0, 10) !== value) {\n    throw new RangeError('Invalid calendar date');\n  }\n}\n\nexport function formatDueDate(value) {\n  requireCivilDate(value);\n  const date = new Date(value);\n  return `${pad(date.getDate())}/${pad(date.getMonth() + 1)}/${date.getFullYear()}`;\n}\n\nexport function formatInstant(value, timeZone) {\n  return new Intl.DateTimeFormat('en-GB', {\n    day: '2-digit', month: '2-digit', year: 'numeric', timeZone,\n  }).format(new Date(value));\n}\n",
    "wrong-global-utc/probe.mjs": "import { formatDueDate, formatInstant } from './date.mjs';\n\nconst input = '2026-10-25';\nconst parsed = new Date(input);\nconsole.log(JSON.stringify({\n  requestedZone: process.env.TZ,\n  resolvedZone: Intl.DateTimeFormat().resolvedOptions().timeZone,\n  input,\n  epochMs: parsed.getTime(),\n  iso: parsed.toISOString(),\n  local: { year: parsed.getFullYear(), month: parsed.getMonth() + 1, day: parsed.getDate() },\n  dueDate: formatDueDate(input),\n  instant: formatInstant('2026-10-25T00:30:00.000Z'),\n}, null, 2));\n",
    "wrong-global-utc/date.test.mjs": "import test from 'node:test';\nimport assert from 'node:assert/strict';\nimport { formatDueDate, formatInstant } from './date.mjs';\n\ntest('civil due date keeps its calendar day', () => {\n  assert.equal(formatDueDate('2026-10-25'), '25/10/2026');\n});\ntest('civil date on the spring clock change keeps its calendar day', () => {\n  assert.equal(formatDueDate('2026-03-29'), '29/03/2026');\n});\ntest('valid leap day is accepted', () => {\n  assert.equal(formatDueDate('2024-02-29'), '29/02/2024');\n});\ntest('non-leap February 29 is rejected', () => {\n  assert.throws(() => formatDueDate('2026-02-29'), RangeError);\n});\ntest('February 30 is rejected', () => {\n  assert.throws(() => formatDueDate('2026-02-30'), RangeError);\n});\ntest('month 13 is rejected', () => {\n  assert.throws(() => formatDueDate('2026-13-01'), RangeError);\n});\ntest('timestamp is rejected by the civil-date function', () => {\n  assert.throws(() => formatDueDate('2026-10-25T00:30:00Z'), RangeError);\n});\ntest('noncanonical date is rejected', () => {\n  assert.throws(() => formatDueDate('2026-1-5'), RangeError);\n});\ntest('a real instant has October 25 in UTC', () => {\n  assert.equal(formatInstant('2026-10-25T00:30:00Z', 'UTC'), '25/10/2026');\n});\ntest('the same real instant has October 24 in New York', () => {\n  assert.equal(formatInstant('2026-10-25T00:30:00Z', 'America/New_York'), '24/10/2026');\n});\ntest('the default instant display keeps the configured application timezone', () => {\n  assert.ok(process.env.EXPECTED_INSTANT_DATE, 'Set EXPECTED_INSTANT_DATE for this run');\n  assert.equal(formatInstant('2026-10-25T00:30:00Z'), process.env.EXPECTED_INSTANT_DATE);\n});\n",
    "README.md": "# Civil-date debugging lab\n\nThis lab diagnoses an incorrect invoice due-date display. The domain value is a civil date, `2026-10-25`: the displayed due date must remain `25/10/2026` in every configured application timezone. A separate event timestamp is an instant and must retain timezone conversion.\n\n## Files and prerequisites\n\nUse Docker Engine or Docker Desktop and a Bash/zsh-compatible terminal. No local Node installation, npm install, database or network service is needed. Each snapshot contains the same three filenames:\n\n```text\nbaseline/\n  date.mjs\n  probe.mjs\n  date.test.mjs\nfixed/\n  date.mjs\n  probe.mjs\n  date.test.mjs\nwrong-global-utc/\n  date.mjs\n  probe.mjs\n  date.test.mjs\nREADME.md\n```\n\n`baseline` is the initial implementation. `fixed` changes only civil-date formatting. `wrong-global-utc` prepends `process.env.TZ = 'UTC'` to the baseline as a deliberately incorrect repair for this application's contract. The tests and probe are identical across snapshots. Create these files from the supplied source bundle, or copy the three baseline files into each directory and apply the shown changes. Run all commands below from the directory containing the three snapshot directories.\n\nThe recorded execution used Node 24.21.0 on Linux arm64, ICU 78.3 and timezone data 2026c. The image was already installed. To obtain the pinned image on another machine, run:\n\n```sh\nLAB_IMAGE='node@sha256:3d27e5c11e5786e309ec3e03f93ae536eb36e6e5eb3714d5eb3300a36157add0'\ndocker pull \"$LAB_IMAGE\"\n```\n\nThe digest identifies the image index; another supported host architecture can select a different platform image. The retained observation is the arm64 execution, not an assertion that every platform was tested.\n\n## Run a controlled case\n\nDefine this helper once in the same terminal. Its first three arguments are snapshot, timezone and expected date for the real event timestamp; remaining arguments go to Node. Containers run without network access and with the source directory mounted read-only. They are removed at exit.\n\n```sh\nrun_case() {\n  LAB_VARIANT=\"$1\"\n  LAB_TZ=\"$2\"\n  LAB_EXPECTED=\"$3\"\n  shift 3\n  docker run --rm --pull never --network none --read-only \\\n    --cap-drop ALL --security-opt no-new-privileges \\\n    --memory 128m --cpus 1 --pids-limit 64 --user node \\\n    --env \"TZ=$LAB_TZ\" --env \"EXPECTED_INSTANT_DATE=$LAB_EXPECTED\" \\\n    --mount \"type=bind,src=$PWD/$LAB_VARIANT,dst=/app,readonly\" \\\n    --workdir /app \"$LAB_IMAGE\" node \"$@\"\n}\n```\n\nOnly change the timezone between these probes. The expected-instant argument is used by the test suite, not by the probe.\n\n```sh\nrun_case baseline UTC 25/10/2026 probe.mjs\nrun_case baseline Europe/Rome 25/10/2026 probe.mjs\nrun_case baseline America/New_York 24/10/2026 probe.mjs\n```\n\nAll three probes report `epochMs: 1792886400000` and `iso: \"2026-10-25T00:00:00.000Z\"`. The local day and due-date display are 25 in UTC/Rome and 24 in New York. That observation rejects the hypothesis that changing `TZ` changed the parsed instant in this example. The difference appears when local calendar components are read.\n\n## Failing regression test and corrected implementation\n\nThe suite has eleven tests. The first three preserve civil dates (October date, March date and valid leap day); the next five reject invalid or noncanonical civil-date inputs; the last three preserve the instant-formatting behaviour. March 29 and October 25 coincide with Rome's 2026 clock changes, but the defect is not limited to daylight-saving transitions.\n\n```sh\nrun_case baseline UTC 25/10/2026 --test date.test.mjs\nrun_case baseline Europe/Rome 25/10/2026 --test date.test.mjs\nrun_case baseline America/New_York 24/10/2026 --test date.test.mjs\n```\n\nExpected: 11 passed in UTC, 11 passed in Rome, 8 passed and 3 failed in New York. The New York command exits with status 1; that failure is the reproduction.\n\n`fixed/date.mjs` retains validation but formats the validated year, month and day directly. UTC is used only for the calendar-validity round trip. Formatting does not convert a civil date into a local date. The independent instant formatter is unchanged.\n\n```sh\nrun_case fixed UTC 25/10/2026 --test date.test.mjs\nrun_case fixed Europe/Rome 25/10/2026 --test date.test.mjs\nrun_case fixed America/New_York 24/10/2026 --test date.test.mjs\n```\n\nExpected: 11 passed, 0 failed and exit status 0 in each zone. The exact same test file is used before and after the change.\n\n## Reject a repair that changes the requirement\n\n```sh\nrun_case wrong-global-utc America/New_York 24/10/2026 --test date.test.mjs\n```\n\nExpected: 10 passed, 1 failed, exit status 1. Forcing the process timezone to UTC masks the civil-date bug but changes the default display of the real instant `2026-10-25T00:30:00Z`: the New York application now displays October 25 instead of October 24. Explicitly selecting New York still works, but the default application-zone behaviour was part of the existing contract. A system intentionally designed to display all instants in UTC has a different requirement.\n\n## Completion and cleanup\n\nThe corrected suite passing in all three zones establishes the tested behaviour for these inputs. It does not measure agent quality, debugging speed or a production incident. No external AI calls are part of this lab.\n\nEvery command uses `--rm`; no named volumes or installed packages are created. Keep the source while reviewing it, then remove only this lab directory if it is no longer needed. Do not prune Docker globally or remove an image that other work already uses.\n"
  },
  "environment": {
    "node": "24.21.0",
    "platform": "linux",
    "architecture": "arm64",
    "icu": "78.3",
    "tzdata": "2026c",
    "image": "node@sha256:3d27e5c11e5786e309ec3e03f93ae536eb36e6e5eb3714d5eb3300a36157add0"
  },
  "sourceHashes": {
    "baseline/date.mjs": "7678538df1e28a4edba2bf960c3e2146360b045804bcdab5df3fc831556ddb14",
    "baseline/probe.mjs": "361cf76d2448efb333a87616d22bf1b0b21df1633bb5aeb7fe6a856d97a8feed",
    "baseline/date.test.mjs": "0c06b9278ac8bba95d0918a7634e5b391b986611ae4491e79416400be543319a",
    "fixed/date.mjs": "667be4340f98ebe7bc424acb89379280f47b0e27caa21766f64bd1a536fb412c",
    "fixed/probe.mjs": "361cf76d2448efb333a87616d22bf1b0b21df1633bb5aeb7fe6a856d97a8feed",
    "fixed/date.test.mjs": "0c06b9278ac8bba95d0918a7634e5b391b986611ae4491e79416400be543319a",
    "wrong-global-utc/date.mjs": "92c06867bc431eaca3aa237497fe619ca2f8e43f9a819c1c197dc03a9c808d36",
    "wrong-global-utc/probe.mjs": "361cf76d2448efb333a87616d22bf1b0b21df1633bb5aeb7fe6a856d97a8feed",
    "wrong-global-utc/date.test.mjs": "0c06b9278ac8bba95d0918a7634e5b391b986611ae4491e79416400be543319a"
  },
  "results": [
    {
      "variant": "baseline",
      "zone": "UTC",
      "exitCode": 0,
      "tests": {
        "tests": 11,
        "pass": 11,
        "fail": 0
      }
    },
    {
      "variant": "baseline",
      "zone": "Europe/Rome",
      "exitCode": 0,
      "tests": {
        "tests": 11,
        "pass": 11,
        "fail": 0
      }
    },
    {
      "variant": "baseline",
      "zone": "America/New_York",
      "exitCode": 1,
      "tests": {
        "tests": 11,
        "pass": 8,
        "fail": 3
      }
    },
    {
      "variant": "fixed",
      "zone": "UTC",
      "exitCode": 0,
      "tests": {
        "tests": 11,
        "pass": 11,
        "fail": 0
      }
    },
    {
      "variant": "fixed",
      "zone": "Europe/Rome",
      "exitCode": 0,
      "tests": {
        "tests": 11,
        "pass": 11,
        "fail": 0
      }
    },
    {
      "variant": "fixed",
      "zone": "America/New_York",
      "exitCode": 0,
      "tests": {
        "tests": 11,
        "pass": 11,
        "fail": 0
      }
    },
    {
      "variant": "wrong-global-utc",
      "zone": "America/New_York",
      "exitCode": 1,
      "tests": {
        "tests": 11,
        "pass": 10,
        "fail": 1
      }
    }
  ],
  "probes": [
    {
      "requestedZone": "UTC",
      "resolvedZone": "UTC",
      "input": "2026-10-25",
      "epochMs": 1792886400000,
      "iso": "2026-10-25T00:00:00.000Z",
      "local": {
        "year": 2026,
        "month": 10,
        "day": 25
      },
      "dueDate": "25/10/2026",
      "instant": "25/10/2026"
    },
    {
      "requestedZone": "Europe/Rome",
      "resolvedZone": "Europe/Rome",
      "input": "2026-10-25",
      "epochMs": 1792886400000,
      "iso": "2026-10-25T00:00:00.000Z",
      "local": {
        "year": 2026,
        "month": 10,
        "day": 25
      },
      "dueDate": "25/10/2026",
      "instant": "25/10/2026"
    },
    {
      "requestedZone": "America/New_York",
      "resolvedZone": "America/New_York",
      "input": "2026-10-25",
      "epochMs": 1792886400000,
      "iso": "2026-10-25T00:00:00.000Z",
      "local": {
        "year": 2026,
        "month": 10,
        "day": 24
      },
      "dueDate": "24/10/2026",
      "instant": "24/10/2026"
    }
  ]
}
