# events.mjs — WAL-журнал подій `flow`

## Огляд

Модуль `events.mjs` реалізує **append-only WAL** (Write-Ahead Log) журнал подій для підсистеми `flow` (специфікація §4.1.2, §9). Журнал зберігається у sibling-файлі поряд із checkout-каталогом worktree: для `…/.worktrees/feat-x` файл подій має шлях `…/.worktrees/feat-x.events.jsonl` (формат JSON Lines — по одному JSON-об'єкту на рядок).

Журнал є **єдиним** для двох категорій подій:

- **Переходи стану** flow (наприклад, `step_started`, `step_completed`, `blocked` тощо).
- **Облік API-викликів** (події типу `api_call`).

Ключові інваріанти та властивості:

- **Append-only**. Нові записи лише дописуються в кінець файлу. Це робить журнал **краш-безпечнішим** за повний перезапис: якщо аварія сталася посередині запису рядка, ушкоджений (торваний) рядок при читанні **толерується** — він просто пропускається, а решта журналу залишається валідною.
- **WAL-інваріант**: подію слід дописати в журнал **ДО** оновлення високорівневого статусу у snapshot-файлі `.flow.json`. За дотримання цього інваріанта відповідає сторонній викликач — `state-store.recordTransition`.
- **Усі шляхи — абсолютні**. Модуль явно валідує кожен вхідний `path` і кидає помилку для відносних шляхів (правило проекту `no-relative-fs-path`).

Модуль використовує **синхронні** Node.js fs-API (`appendFileSync`, `readFileSync`, `existsSync`), що адекватно для коротких записів короткоживучих CLI/dispatcher-сценаріїв і гарантує лінійний порядок без race-conditions у межах одного процесу.

## Експорти / API

Усі експорти — іменовані ESM-функції.

| Експорт                                | Тип                                         | Призначення                                                                                         |
| -------------------------------------- | ------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `flowEventsPath(worktreeDir)`          | `(string) => string`                        | Обчислити абсолютний шлях sibling-журналу для checkout-каталогу worktree.                           |
| `appendEvent(eventsPath, event, now?)` | `(string, object, () => number?) => object` | Дописати одну подію (зі стампом `at`) у журнал; повертає фактично записаний об'єкт.                 |
| `readEvents(eventsPath)`               | `(string) => object[]`                      | Прочитати всі події з журналу в порядку запису; відсутній файл → `[]`; невалідні рядки ігноруються. |

Default-експорт відсутній.

## Функції

### `flowEventsPath(worktreeDir)`

**Сигнатура.** `function flowEventsPath(worktreeDir: string): string`

**Параметри.**

- `worktreeDir` — **абсолютний** шлях до checkout-каталогу worktree (наприклад, `/repo/.worktrees/feat-x`).

**Повертає.** Абсолютний шлях до sibling-файлу журналу подій: каталог-батько (`dirname(worktreeDir)`) + базове ім'я worktree + суфікс `.events.jsonl`. Наприклад, для входу `/repo/.worktrees/feat-x` повертається `/repo/.worktrees/feat-x.events.jsonl`.

**Помилки.** Якщо `worktreeDir` не є абсолютним шляхом — кидає `Error` з префіксом `flowEventsPath: очікується абсолютний шлях …`.

**Side effects.** Жодних. Чиста функція (працює лише з рядками через `path`-утиліти).

**Деталі реалізації.** Використовує `dirname` і `basename` з `node:path`, що робить функцію крос-платформною щодо роздільників шляху (хоча в результаті завжди формується шлях у стилі поточної ОС).

### `appendEvent(eventsPath, event, now?)`

**Сигнатура.** `function appendEvent(eventsPath: string, event: object, now?: () => number): object`

**Параметри.**

- `eventsPath` — **абсолютний** шлях до файлу `.events.jsonl`.
- `event` — об'єкт події, наприклад `{ type: 'step_started', step: 2 }`. Може містити довільні поля.
- `now` — _необов'язкова_ фабрика часу, що повертає мілісекунди (за замовчуванням `Date.now`). Дозволяє **ін'єкцію** детермінованого часу у тестах.

**Повертає.** Фактично записаний запис у формі `{ at, ...event }`, де `at` — ISO-рядок часу (`new Date(now()).toISOString()`). Поля переданого `event` накладаються **поверх** `at`, тому якщо в `event` уже є власне поле `at`, воно перепише згенероване (це може бути корисно для бек-філу/міграцій).

**Side effects.**

- **Файлова система**: дописує один рядок (`JSON.stringify(record) + '\n'`) у `eventsPath` через `appendFileSync` із кодуванням `utf8`. Якщо файлу не існує — він **створюється** автоматично (`appendFileSync` діє як append-or-create).
- **Жодних мережевих, БД- чи stdout-ефектів.**

**Помилки.**

- Якщо `eventsPath` не абсолютний — кидає `Error` з префіксом `appendEvent: очікується абсолютний шлях …`.
- Будь-яка помилка `appendFileSync` (відсутня тека-батько, права, EROFS тощо) пробрасується назовні як є.

**Краш-безпека.** Якщо процес аварійно завершиться під час `appendFileSync`, у файлі може залишитися **частково записаний останній рядок**. Це навмисно толерується читачем (`readEvents`), тому WAL не вимагає fsync чи двофазного запису для базової стійкості.

### `readEvents(eventsPath)`

**Сигнатура.** `function readEvents(eventsPath: string): object[]`

**Параметри.**

- `eventsPath` — **абсолютний** шлях до файлу `.events.jsonl`.

**Повертає.** Масив об'єктів-подій у тому самому порядку, в якому вони були дописані (хронологічний порядок append-only). Якщо файлу немає — повертається `[]`.

**Side effects.** Лише читання файлу (`readFileSync`, `existsSync`). Файл **не модифікується** і **не створюється**.

**Поведінка при пошкодженнях.**

- Порожні рядки (включно з трейлінговим `\n` після останнього запису) **відфільтровуються** через `line.trim() !== ''`.
- Кожен непорожній рядок намагається парситися як JSON. Якщо `JSON.parse` кидає виняток (наприклад, торваний останній рядок після краху під час `appendFileSync`), цей рядок **пропускається** (через `flatMap` + порожній масив у `catch`), і читання продовжується далі. Інші рядки повертаються нормально.
- Це і є **append-only толерантність**: один пошкоджений запис не валить весь журнал.

**Помилки.** Якщо `eventsPath` не абсолютний — кидає `Error` з префіксом `readEvents: очікується абсолютний шлях …`. Інші помилки fs (наприклад, EACCES при `readFileSync`) пробрасуються.

**Складність.** Лінійна за розміром файлу (`O(N)`), читання здійснюється повністю в пам'ять. Для дуже довгих журналів варто розглянути потокове читання — модуль навмисно тримає просту синхронну реалізацію.

## Залежності

### Зовнішні (Node.js stdlib)

- `node:fs`
  - `appendFileSync` — атомарний (на рівні syscall) append одного запису.
  - `existsSync` — перевірка наявності файлу перед читанням.
  - `readFileSync` — повне читання файлу журналу.
- `node:path`
  - `basename`, `dirname`, `isAbsolute`, `join` — формування sibling-шляху і валідація абсолютних шляхів.

### Внутрішні

Модуль **не має внутрішніх імпортів**. Він — найнижчий рівень WAL-абстракції, на який спираються інші частини dispatcher/flow:

- `state-store.recordTransition` (споживач) — викликає `appendEvent` перед оновленням `.flow.json` (забезпечення WAL-інваріанта).
- Будь-який код обліку `api_call`-подій також використовує `appendEvent`.
- Читачі (наприклад, інструменти діагностики, відновлення стану, аналітики) використовують `readEvents`.

## Потік виконання / Використання

### Типовий життєвий цикл подій worktree

1. **Обчислення шляху журналу** — один раз на сесію/процес:
   ```js
   import { flowEventsPath, appendEvent, readEvents } from './events.mjs'
   const eventsPath = flowEventsPath('/repo/.worktrees/feat-x')
   // → '/repo/.worktrees/feat-x.events.jsonl'
   ```
2. **Дозапис події під час переходу стану** (через `state-store.recordTransition`, що дотримується WAL-інваріанта):
   ```js
   appendEvent(eventsPath, { type: 'step_started', step: 2 })
   // потім: оновити .flow.json
   ```
3. **Дозапис обліку API-виклику** (тим самим API, у тому ж файлі):
   ```js
   appendEvent(eventsPath, {
     type: 'api_call',
     provider: 'anthropic',
     model: 'claude-opus',
     tokens: 1234
   })
   ```
4. **Читання історії** (наприклад, для відновлення стану або аудиту):
   ```js
   const events = readEvents(eventsPath)
   for (const ev of events) {
     // обробка ev.at, ev.type, …
   }
   ```

### Сценарій краху

1. `appendFileSync` починає писати рядок, процес гине посеред запису.
2. Файл містить N−1 валідних рядків + один торваний (наприклад, без `}` і `\n` у кінці).
3. На наступному старті `readEvents`:
   - відфільтрує порожні рядки;
   - спробує `JSON.parse` для кожного непорожнього;
   - на торваному останньому отримає виняток → `flatMap` поверне `[]` для нього → запис **пропускається**;
   - решта N−1 подій нормально потрапить у результат.
4. Дані до моменту краху збережено; пошкоджений запис вважається втраченим — це прийнятна семантика append-only WAL.

### Інваріанти, що накладає модуль

- **Усі шляхи — абсолютні** (`isAbsolute` перевірка у всіх трьох експортах).
- **Кодування файлу — UTF-8** (явно вказано як в `appendFileSync`, так і в `readFileSync`).
- **Формат рядка — однорядковий JSON + `\n`** (jsonl).
- **`at` — ISO-8601-рядок UTC** (через `Date.prototype.toISOString`).
- **Порядок збереження — порядок дозапису** (нативна семантика append-only).

### Чого модуль свідомо не робить

- Не реалізує **rotation / compaction** журналу — це відповідальність вищих рівнів або інфраструктури worktree.
- Не виконує **fsync** після кожного append — покладається на ОС-кеш та append-only толерантність читача.
- Не валідує **схему події** — приймає будь-який `object` (контрактом структури подій відає `flow`-специфікація і споживачі).
- Не обробляє **багатопроцесний конкурентний доступ** — для одного worktree передбачається один пишучий процес; для читача конкурентність безпечна, оскільки `appendFileSync` атомарно дозаписує цілий буфер.

## Rebuild Test

Документація достатня, щоб переписати модуль з нуля:

- три іменовані експорти з точними сигнатурами;
- формула sibling-шляху (`dirname` + `basename` + `.events.jsonl`);
- WAL-інваріант і його носій (`state-store.recordTransition`);
- формат запису (`{ at: ISO, ...event }`, `JSON.stringify` + `\n`, кодування UTF-8);
- стратегія читання (`existsSync`-гард → split `\n` → фільтр порожніх → `JSON.parse` у `try/catch` + `flatMap`);
- правила валідації абсолютних шляхів і повідомлення про помилки.
