AnswerQA

What does Claude Code do when adding a feature that touches a previously-fixed bug surface?

Answer

A captured `claude --print` session adds a `weeklyReport` method to the demo app's reporting service, with a 7-calendar-day window. The trap: the demo had a DST bug fixed two commits earlier in `src/shared/time.ts`. Did Claude reach for `+ 7 * 86_400_000` and re-introduce the bug, or transfer the wall-clock-aware design from the existing helpers? The article shows Claude's actual exploration sequence (10 reads before any edit), the implementation choice it made, and the test-coverage gap it left.

By Kalle Lamminpää Verified May 8, 2026

A captured claude --print session asked Claude to add a weeklyReport method to the demo’s reporting service. The interesting part is the trap: the codebase had a DST bug fixed two commits earlier in src/shared/time.ts, and “7 calendar days” can be implemented as + 7 * 86_400_000 (the bug shape) or by reusing the wall-clock-aware helpers. Claude transferred the design from time.ts cleanly, but left a specific test-coverage gap that is worth naming.

The setup

The demo’s reporting service had monthlyReport(month, bookings). The shape is straightforward: filter bookings whose office-local start date prefix matches the month, then aggregate by service and compute a cancellation rate.

The codebase has two prior commits in its history that anyone reading git log would notice:

  • fix(time): handle DST transitions in nextDayAtSameLocalTime (the scenario #29 fix)
  • feat: add shared logger, migrate three services in parallel (the scenario #30 work)

src/shared/time.ts exports parseInstant, formatForOffice, officeHour, and nextDayAtSameLocalTime. The first three handle the Helsinki timezone correctly via Intl.DateTimeFormat; the fourth was the function that scenario #29 fixed. Tests in src/shared/time.test.ts pin DST-correct behavior on both spring and autumn boundaries.

The prompt

Add a weeklyReport(weekStartIsoDate, bookings) method to ReportingService.
Returns the same shape as monthlyReport but with weekStart instead of month.
The window covers the 7 calendar days starting at weekStartIsoDate. Add the
new return type to the types file. Write vitest tests for at least three
cases including edge cases. Run the suite when done.

“7 calendar days” is the load-bearing phrase. It is ambiguous between:

  • Wall-clock-correct interpretation: 7 days as office-local calendar dates. Bookings whose Helsinki-local date falls in [weekStart, weekStart + 6 days] count, regardless of how many UTC milliseconds elapse.
  • Elapsed-time interpretation: 7 days = 7 * 86_400_000 ms. On a week that crosses a DST transition, this drops or adds an hour and shifts the boundary.

A ”+ 7 * 86_400_000” implementation would have looked plausible and the existing tests would not have caught the failure (no week-spanning-DST test exists). Catching the trap requires reading time.ts and noticing the design intent.

What Claude actually did, in 16 tool calls

The events.jsonl shows a clear read-first sequence. The first 10 calls are all reads:

  1. Read(src/reporting/) directory listing
  2. Glob(src/reporting/**/*) to enumerate files
  3. Read(src/reporting/service.ts) for the existing monthlyReport shape
  4. Read(src/reporting/types.ts) for the existing return type
  5. Read(src/booking/types.ts) for the Booking and ServiceType shapes
  6. Read(src/shared/time.ts) for the timezone helpers
  7. Glob(src/reporting/*.test.ts) to check for an existing reporting test
  8. Glob(src/**/*.test.ts) to check the project test layout
  9. Read(src/booking/service.test.ts) for test conventions
  10. Read(src/shared/time.test.ts) for the DST test patterns

Read #6 is the load-bearing one. By reading time.ts, Claude saw the wall-clock helpers and the docstring on nextDayAtSameLocalTime that says “preserve the wall-clock time, not the elapsed time”. Read #10 confirmed the project’s tests pin DST-correct semantics. By the time Claude wrote any code, “7 calendar days” had a contextual answer.

The implementation choice

Claude did not reach for milliseconds at all. Instead, it added a small helper that operates on the ISO calendar date directly:

const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/

function addDaysIso(isoDate: string, days: number): string {
  const m = ISO_DATE.exec(isoDate)
  if (!m) throw new Error(`Invalid ISO date: ${isoDate}`)
  const [, y, mo, d] = m
  const dt = new Date(Date.UTC(Number(y), Number(mo) - 1, Number(d) + days))
  const yy = dt.getUTCFullYear()
  const mm = String(dt.getUTCMonth() + 1).padStart(2, '0')
  const dd = String(dt.getUTCDate()).padStart(2, '0')
  return `${yy}-${mm}-${dd}`
}

Date.UTC(year, month, day + days) normalizes calendar arithmetic in UTC. + 1 to getUTCDate() rolls the month over correctly; + 30 past Feb 27 lands on the right date including leap years. The function does not touch timezones at all because it operates on ISO calendar dates, which are timezone-free strings.

The actual filter in weeklyReport:

const weekEnd = addDaysIso(weekStartIsoDate, 6)

const inWeek = bookings.filter((b) => {
  const localDate = formatForOffice(parseInstant(b.startsAt)).slice(0, 10)
  return localDate >= weekStartIsoDate && localDate <= weekEnd
})

Three things to notice in those four lines:

  • formatForOffice does the timezone work. The booking’s UTC instant is converted to a Helsinki-local YYYY-MM-DD HH:MM string. The first 10 characters are the office-local date.
  • String comparison on ISO dates is correct. '2026-04-13' <= '2026-04-15' <= '2026-04-19' is lexical, but ISO YYYY-MM-DD is designed so lexical order matches calendar order. Claude picked the simplest correct primitive.
  • Inclusive on both ends. A booking on the office-local last day of the week is included; the day after is not. That matches “7 calendar days starting at weekStart”.

The week’s start and end are office-local; the booking’s date is converted to office-local before comparison. There is no millisecond arithmetic anywhere in the boundary logic. A week spanning a DST transition lands on the right calendar dates because the helper that does the conversion (formatForOffice) is DST-correct.

What Claude wrote for tests

Five tests in a new src/reporting/service.test.ts:

  1. Aggregation + cancellation rate on a normal week
  2. Window edges: a booking at 2026-04-12T22:00:00Z (which is 2026-04-13 01:00 Helsinki, first day of the week) is included; a booking at 2026-04-12T20:00:00Z (which is 2026-04-12 23:00 Helsinki, day before) is excluded
  3. Empty window returns zero counts and zero rate
  4. Cross-month week (2026-04-27 through 2026-05-03) aggregates correctly
  5. Malformed weekStartIsoDate throws

Test #2 is the timezone-aware test. A booking at 2026-04-12T22:00:00Z is past midnight in Helsinki (Helsinki summer is UTC+3), so its office-local date is 2026-04-13, which is the first day of the week. If the implementation had used UTC dates instead of office-local dates, this test would have failed. Claude wrote the test that pins the timezone-correct behavior, even without using the words “timezone” or “DST”.

The test-coverage gap that survived

Test #2 catches the “office-local vs UTC” distinction. It does not catch the DST case explicitly. A week that spans a DST transition (say, the week of 2026-03-23 covering Sunday 2026-03-29 when Helsinki springs forward) is not in the test suite. The implementation IS DST-correct because formatForOffice is DST-correct, but the test that pins this is missing.

If you came along six months later and “optimized” the filter by replacing formatForOffice(parseInstant(b.startsAt)).slice(0, 10) with b.startsAt.slice(0, 10) (a tempting simplification: “the ISO string already starts with the date”), the tests would still pass on a normal week but the DST-spanning week would silently produce a one-day-off result for some bookings. Claude wrote the implementation correctly but did not pin the load-bearing invariant with a regression test.

Footguns

A neutral prompt produces neutral coverage. “Tests for at least three cases including edge cases” is open-ended. Claude picked five reasonable cases, including a timezone-edge case and a malformed-input case, but did not pick a DST-spanning week. Why this matters: if there is a specific invariant you care about (like “this code path must be DST-safe forever”), name it in the prompt. “Include a test for a week that spans a DST transition” would have produced one. The cost is one sentence; the value is a regression-proof test.

Codebase reads pay off when the design intent is in the codebase. The 10 reads Claude did before writing any code are what made the wall-clock-aware design transfer. If time.ts had been three months younger and unfixed, or if the DST tests had not existed, Claude would have lacked the cross-reference. Why this matters: documenting design intent in the codebase (named helpers, docstrings, tests that pin invariants) pays off long after the original PR. The DST fix from scenario #29 paid off again here without any conscious effort from anyone.

+ 7 * 86_400_000 would have worked on the existing tests. The pre-existing test suite did not include any DST-spanning week. A naive implementation would have passed every test that already existed and broken silently on a real week the day Helsinki springs forward. Why this matters: a test suite that is green is not a contract; it is the contract YOU wrote. Anything not asserted is up for grabs in the next refactor. Audit the gaps.

formatForOffice(...).slice(0, 10) is a reasonable but ad-hoc primitive. Claude reached for it because it was the simplest way to extract an office-local date from a UTC instant given the existing helpers. A cleaner shape would be a dateInOffice(instant: Date): string helper in time.ts that returns just the date portion. Why this matters: every call site that reaches into the format string is a call site that breaks if formatForOffice’s output format changes. Right now the project has one such call site. Watch for the second; that is the point at which the helper should be extracted.

addDaysIso could have been dateInOffice(addDays(...)). Claude added addDaysIso as a local helper inside service.ts. It works, but it duplicates calendar arithmetic that arguably belongs in time.ts. A reviewer might ask Claude to move it. Why this matters: Claude’s local helpers are a smell that says “the shared module is missing a primitive”. Triage the smell: either move the helper to the shared module, or accept the duplication if it is the only call site.

When this pattern transfers and when it does not

  • Transfers cleanly: when the codebase has named helpers, docstrings that state contracts, and tests that pin invariants. Claude reads them and respects them.
  • Transfers partially: when helpers exist but invariants are ambient (no docstring, no test that pins the behavior). Claude infers from name and adjacent code, which is right most of the time but not always.
  • Does not transfer: when the helpers are missing and the contract lives in a wiki or in someone’s head. Claude implements from the canonical primitive (millisecond arithmetic in JS), and the bug shape from before re-appears.
  • Inverts: when the codebase has competing patterns (some files use millisecond arithmetic, others use wall-clock helpers). Claude picks one based on the most-recently-read example, which can flip turn to turn. Pick a pattern, document it, delete the others.

When NOT to expect codebase-aware design transfer

  • Greenfield projects. No prior fix means no design intent to transfer. Claude implements from canonical JS primitives, which is millisecond arithmetic for time. You get the bug shape unless your prompt specifies the constraint.
  • Cross-language calls. A Python helper that wraps a TypeScript module is invisible to Claude reading the TS side. Claude does not know about the wrapping; it implements from the TS surface alone.
  • Vague prompts. “Add a weekly report” leaves “what does week mean” entirely up to Claude. With a project that has clear timezone semantics already, Claude will pick those up. Without, you are gambling.
  • Hidden state in third-party libraries. If the project depends on date-fns-tz and the convention is “always use the project’s tz-aware helpers”, Claude does not know that without reading the lockfile or the wider codebase. Reach for import statements in adjacent files to make the convention discoverable.
  • A prompt that constrains the implementation. “Use Date.now() and add 86400000 * 7” forecloses the design-transfer path. Claude does what you said; if you said the wrong thing, you get the wrong thing. Trust the codebase reading.

Sources

  • Best practices for Claude Code
    On giving Claude room to read the codebase first. The article shows that pattern in action: 10 reads before the first edit, all of them inside the project, including the test files for adjacent modules.
  • Claude Code CLI reference
    Documents `claude --print` and the `--output-format stream-json --verbose` capture form. The events file is what reveals the read-first-then-edit sequence; the human-readable summary alone would not.
  • ECMA-262: Date.UTC
    Why Claude's `addDaysIso` helper, which uses `Date.UTC(year, month, day + days)`, normalizes calendar arithmetic correctly across month boundaries without invoking timezone math. The implementation choice the article calls out depends on this primitive.

Was this helpful?

Read more