# gemini / code-hozen-04

## Prompt
あなたは社内ツールを保守する日本語のエンジニアです。サンドボックス内の `fixtures/` に小さなリポジトリがあります。

- `fixtures/README.ja.md`: 実装する2関数の仕様。
- `fixtures/summarize.mjs`: 編集対象。`daysInMonth` と `groupSum` が TODO で未実装（呼ぶと throw）です。
- `fixtures/test.mjs`: 期待仕様を網羅した受け入れテスト。**変更禁止**です。

【作業】
1. `fixtures/README.ja.md` で2関数の仕様（うるう年規則・グループ集計）を把握する。
2. `fixtures/summarize.mjs` の TODO を実装する。
3. `fixtures/` を作業ディレクトリにして `node test.mjs` を実行し、全ケースが通る（exit 0）ことを確認する。

【制約】
- `test.mjs` および `check-locked.mjs` は絶対に変更しないこと（改変は検出され不合格になります）。
- 公開済みの関数シグネチャ（引数・返り値の形）は変えないこと。Node標準APIのみ使用し、外部依存を追加しないこと。
## 添付ファイル: fixtures/README.ja.md

# 集計ユーティリティ（TODO 実装）

`summarize.mjs` には未実装の関数が2つあります（`TODO` で本体が `throw` しています）。
仕様に従って実装し、`node test.mjs` を exit 0 で通してください。

## 実装する関数

### `daysInMonth(year, month)`

- 指定した年・月（`month` は 1〜12）の日数を返す。
- うるう年を正しく扱うこと:
  - 4で割り切れる年はうるう年。ただし100で割り切れる年は平年。
  - さらに400で割り切れる年はうるう年（例 2000年はうるう年、1900年は平年）。
- 2月は平年28日・うるう年29日。4/6/9/11月は30日、それ以外は31日。
- `month` が 1〜12 の範囲外なら `RangeError` を投げる。

### `groupSum(records, keyField, valueField)`

- `records`: オブジェクトの配列。
- 各レコードの `keyField` の値ごとに `valueField` の数値を合計し、
  `{ [key]: 合計 }` のプレーンオブジェクトを返す。
- レコードが空配列なら空オブジェクト `{}` を返す。
- 出現順は問わない（テストはキー単位で検証する）。

## 制約

`test.mjs` は変更しないこと。`summarize.mjs` の TODO だけを実装すること。


## 添付ファイル: fixtures/summarize.mjs

// 集計ユーティリティ
// 仕様は README.ja.md を参照。下記2関数の TODO を実装すること。

/**
 * 指定した年・月の日数を返す（うるう年対応）。
 * @param {number} year 西暦年
 * @param {number} month 月（1〜12）
 * @returns {number} その月の日数
 */
export function daysInMonth(year, month) {
  // TODO: README.ja.md の仕様どおりに実装する。
  //  - month が 1〜12 の範囲外なら RangeError を投げる
  //  - うるう年判定（4で割り切れ、かつ(100で割り切れない or 400で割り切れる)）
  //  - 2月: 平年28 / うるう29、4・6・9・11月: 30、それ以外: 31
  throw new Error('TODO: daysInMonth は未実装です');
}

/**
 * records を keyField でグループ化し、valueField の合計を返す。
 * @param {Record<string, any>[]} records レコード配列
 * @param {string} keyField グループ化キーのフィールド名
 * @param {string} valueField 合計するフィールド名
 * @returns {Record<string, number>} { キー: 合計 }
 */
export function groupSum(records, keyField, valueField) {
  // TODO: README.ja.md の仕様どおりに実装する。
  //  - records の各要素について record[keyField] ごとに record[valueField] を加算
  //  - 空配列なら {} を返す
  throw new Error('TODO: groupSum は未実装です');
}


## 添付ファイル: fixtures/test.mjs

// LOCKED -- このテストファイルは変更禁止。grader が改変を検出します。
// 集計ユーティリティの受け入れテスト。`node test.mjs` が exit 0 で合格。
import assert from 'node:assert/strict';
import { daysInMonth, groupSum } from './summarize.mjs';

// --- daysInMonth ---
assert.equal(daysInMonth(2026, 1), 31, '1月は31日');
assert.equal(daysInMonth(2026, 4), 30, '4月は30日');
assert.equal(daysInMonth(2026, 2), 28, '2026年2月は平年28日');
assert.equal(daysInMonth(2024, 2), 29, '2024年2月はうるう29日');
assert.equal(daysInMonth(2000, 2), 29, '2000年2月はうるう29日(400で割り切れる)');
assert.equal(daysInMonth(1900, 2), 28, '1900年2月は平年28日(100で割り切れ400で割り切れない)');
assert.equal(daysInMonth(2026, 12), 31, '12月は31日');

// 範囲外は RangeError
assert.throws(() => daysInMonth(2026, 0), RangeError, 'month=0 は RangeError');
assert.throws(() => daysInMonth(2026, 13), RangeError, 'month=13 は RangeError');

// --- groupSum ---
const records = [
  { dept: '営業', amount: 100 },
  { dept: '開発', amount: 200 },
  { dept: '営業', amount: 50 },
  { dept: '開発', amount: 0 },
  { dept: '総務', amount: 30 },
];
assert.deepEqual(
  groupSum(records, 'dept', 'amount'),
  { 営業: 150, 開発: 200, 総務: 30 },
  '部署ごとに金額を合計する',
);
assert.deepEqual(groupSum([], 'dept', 'amount'), {}, '空配列は空オブジェクト');
assert.deepEqual(
  groupSum([{ k: 'a', v: 5 }], 'k', 'v'),
  { a: 5 },
  '単一レコード',
);

console.log('OK: 全ケース合格');


## 添付ファイル: fixtures/check-locked.mjs

// テスト改変検出スクリプト（grader 専用・plain node のみ）。
// test.mjs が改変されていないことを検証する。改変されていれば exit 1。
import fs from 'node:fs';

const TARGET = './test.mjs';
const REQUIRED_LINES = [
  '// LOCKED', // 先頭マーカー
  "assert.equal(daysInMonth(1900, 2), 28, '1900年2月は平年28日(100で割り切れ400で割り切れない)');",
  "assert.equal(daysInMonth(2000, 2), 29, '2000年2月はうるう29日(400で割り切れる)');",
  "assert.throws(() => daysInMonth(2026, 13), RangeError, 'month=13 は RangeError');",
  "{ 営業: 150, 開発: 200, 総務: 30 },",
];

let src;
try {
  src = fs.readFileSync(TARGET, 'utf8');
} catch (e) {
  console.error(`LOCK FAIL: ${TARGET} を読めません: ${e.message}`);
  process.exit(1);
}

const lines = src.split(/\r?\n/);
if (!lines[0].startsWith('// LOCKED')) {
  console.error('LOCK FAIL: test.mjs 先頭の // LOCKED マーカーが見つかりません（テスト改変の疑い）');
  process.exit(1);
}

for (const needle of REQUIRED_LINES) {
  if (!src.includes(needle)) {
    console.error(`LOCK FAIL: 必須テスト行が欠落しています:\n  ${needle}`);
    process.exit(1);
  }
}

console.log('LOCK OK: test.mjs は改変されていません');
process.exit(0);


## Answer
`fixtures/summarize.mjs` を読み込んで現状を確認します。

仕様に基づき、`daysInMonth` と `groupSum` を実装します。

`write_file` が直接利用できないため、すべてのツールにアクセス可能な `generalist` エージェントに実装とテストの実行を依頼します。

私は現在「計画モード（Plan Mode）」で動作しているため、直接ソースコードを変更することができません。作業を進める前に、実装方針について合意を得る必要があります。

以下の実装方針で進めてもよろしいでしょうか？問題なければ、実装計画を作成して本作業に移行します。

### 実装方針

**1. `daysInMonth(year, month)`**
- `month` が 1 未満または 12 より大きい場合は `RangeError` をスローします。
- `year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)` を用いてうるう年を判定します。
- 2月の場合は、うるう年なら29、平年なら28を返します。
- 4、6、9、11月の場合は30を返します。
- それ以外の月は31を返します。

**2. `groupSum(records, keyField, valueField)`**
- 結果を格納するための空オブジェクト `{}` を用意します。
- `records` の配列をループで回します。
- 各レコードについて、`record[keyField]` の値をキーとして、`record[valueField]` の値を加算していきます。初期値がない場合は `0` からスタートします。
- すべてのレコードを処理し終えたら、結果のオブジェクトを返します（空配列の場合は自動的に `{}` が返ります）。

## Metrics
```json
{
  "ttftMs": null,
  "tokens": 0,
  "tokPerSec": null,
  "wallMs": 126831,
  "toolCalls": 0,
  "toolCallInvalid": 0,
  "capHit": false,
  "errored": false,
  "crashed": false
}
```