← Назад до блогу

UK/EN Digest, approval hashes і атомарний MDX export

UK/EN Digest проходить approval hash, атомарний export і rollback

Digest changed after approval; approve it again.

Це повідомлення з’явилося, коли ми спробували експортувати Digest, який уже мав approval, але після погодження був змінений. Система не намагалася визначити, чи правка «достатньо маленька». Вона зупинила export.

На перший погляд це виглядає як зайва бюрократія для Markdown-файла. Насправді approval hash відповідає на просте питання: чи збігається поточний editorial artifact із тим станом, який перевірила й погодила людина?

Hash не доводить, що текст фактично правильний. Він не перевіряє першоджерела й не замінює reviewer. Але без нього approval перетворюється на нечітку пам’ять: «здається, я вже бачив цю версію». Для двомовної публікації ризик ще більший. Український текст може змінитися після генерації English adaptation; tags або Recommended Actions можуть розійтися; один MDX може записатися, а другий — впасти на validation.

Тому Digest у Engineering Radar — не два незалежні Markdown-файли й не «переклад за кнопкою». Це editorial transaction зі state machine, двома approvals, identity parity, atomic export і окремим site delivery gate.

У цій статті «ми» — це я та AI coding agent. Я задавав editorial authority, publication boundaries і acceptance criteria. Агент допомагав перетворювати їх на contracts, commands, rollback logic, тести й документацію. Як і в попередніх частинах, ми використовували MAE-підхід: кожна автоматизація мала довести не лише happy path, а й поведінку при stale state, validation failure та частково виконаній операції.

Ranking не визначає склад публікації

Weekly flow починається зі створення Cycle для ISO week. Radar формує candidate selection у визначеному порядку:

text
Critical
  → Saved
  → relevance
  → Primary authority
  → diversity

Ignored, pending або ще не проаналізовані Signals не входять автоматично. Те саме стосується undated Signals: їх можна включити лише окремою manual command. Critical Signals можуть перевищити звичайний limit і не споживають diversity quota.

Ці правила потрібні для повторюваного triage, але ranking не має publication authority. Людина відкриває Weekly Cycle, перевіряє membership, додає пропущені матеріали й лише потім переводить selection із proposed у confirmed.

Після confirmation склад не можна змінювати мовчки. Якщо membership змінився, reviewer має побачити новий state до генерації Digest. Це важлива межа між recommendation engine і editorial decision: алгоритм пропонує candidate set, людина відповідає за те, що саме стане публічним матеріалом.

Український Digest — editorial source

Ukrainian generation дозволена тільки для confirmed selection. У зафіксованій v0.1 configuration Weekly Digest використовував OpenAI Responses API, store: false, strict schema і gpt-5.6-terra як default model для synthesis.

Модель отримує структуровані Signal facts, source URLs і поточні Recommended Actions. Вона не повинна відкривати нову фазу research або додавати факти, яких немає у Signals. Результатом стає draft-uk, а не публікація.

text
confirmed weekly selection

draft-uk
        ↓ human editorial review
approved-uk
        ↓ export
exported-uk

Reviewer перевіряє title, introduction, кожну Signal card, category engineering-radar, tags, Recommended Actions, source links і блоки Було → Стало.

Останній елемент має окремий evidence boundary. Radar може порівнювати тематично пов’язані Signals, але публічний Було → Стало дозволений лише тоді, коли існує підтверджений published evolution record. Тематична схожість сама по собі не є доказом еволюції.

Після редакторських правок UK Digest переходить до approval. Саме цей artifact стає source of truth для English adaptation.

Що фіксує approval hash

Для hash система будує canonical Digest. До нього входять editorial frontmatter і normalized Markdown body. Operational fields, які змінюються внаслідок workflow, не повинні змінювати content identity. Тому approvalHash, export і mutable status виключаються з canonical input.

Скорочено механізм виглядає так:

ts
canonical = editorialFrontmatter + normalize(body)
approvalHash = sha256(canonical)
 
if (sha256(currentCanonical) !== approvalHash) {
  throw new Error("Digest changed after approval")
}

Під час approval SHA-256 записується у frontmatter. Перед export система обчислює його повторно. Якщо title, Signal membership, tags, actions або body були змінені, hashes не збігаються і export блокується.

У W33 R2 український та англійський Digests мали окремі current approval hashes. У публічному поясненні достатньо скорочених форм на кшталт c921… і 827d…: значення важливі не самі по собі, а як зв’язок між конкретними editorial states.

Hash не каже, що reviewer прийняв правильне рішення. Він каже, що система експортує саме той state, до якого це рішення було застосоване.

English adaptation прив’язана до UK state

English Digest генерується з погодженого або вже експортованого UK Digest і тих самих структурованих Signals. Він зберігає membership і порядок Signals, source URLs, tags, Recommended Actions та identity metadata.

Модель може створити idiomatic English adaptation, але не може змінити фактичний склад матеріалу. Поля, які визначають identity, примусово успадковуються з UK, а не приймаються як вільний model output.

EN frontmatter містить sourceDigestId, sourceLanguage: uk і sourceApprovalHash. Це означає: англійський artifact походить не від абстрактного «останнього українського файла», а від конкретно погодженого UK state.

Водночас EN проходить власний lifecycle:

text
approved-uk

draft-en
    ↓ human language and parity review
approved-en
    ↓ export
exported-en

Власний EN approval hash потрібний, тому що мовна адаптація теж є editorial artifact. Reviewer перевіряє факти, тон, source URLs, ordering, actions і те, що текст не додав нової інформації.

Independent EN approval не робить EN незалежним source of truth. Він підтверджує, що похідний artifact перевірено проти конкретного UK source state. Якщо UK змінюється, старий EN більше не може вважатися актуальним: UK потрібно повторно погодити, а EN — згенерувати й перевірити знову.

Identity parity — що має збігатися

Після bilingual export uk.mdx і en.mdx мають спільні:

  • slug;
  • publication date;
  • category engineering-radar;
  • tags;
  • Recommended Actions;
  • cover source і dimensions;
  • locales: [uk, en].

Title, excerpt, cover alt і body локалізуються. Решта полів визначає одну публікацію у двох мовах.

Це також захищає taxonomy. Категорія engineering-radar зарезервована виключно для Digest-публікацій та action filters. Статті цієї серії про реалізацію Radar виходять у engineering-architecture. EN model не може випадково перетворити Digest на інший тип content або створити нову назву action.

Exporter не публікує

Portable MDX contract перевіряє title до 60 символів, excerpt до 160, taxonomy, actions, locales, cover contract і обов’язкове draft: true. Потім запускається native loader зовнішнього site repository.

Exporter не виконує Git commands і не змінює draft на false. Ця separation of authority є навмисною:

text
approved Digest
  ≠ exported MDX
  ≠ publishable MDX
  ≠ committed change
  ≠ merged change
  ≠ deployed article

Obsidian plugin має право підготувати валідний draft у погодженій структурі. Але рішення зробити його публічним належить окремому site delivery flow: local preview, quality gate, draft: false, Git branch, commit, PR, merge, deployment і production smoke.

Це може здаватися довгим ланцюжком. Проте кожна ланка відповідає на інше питання. Approval доводить content identity. Export — відповідність contract. Git — історію зміни. Merge — прийняття в repository. Deployment — доставку build. Production smoke — фактичну доступність сторінки.

У Part 3 ми отримали практичне нагадування: PR був merged, але production deployment заблокував Vercel access rule. Без окремого smoke ми могли б назвати статтю опублікованою, хоча routes повертали 404.

Два MDX мають бути однією транзакцією

Перший UK export створює uk.mdx із locales: [uk]. EN export дозволений лише після нього. Для bilingual pair він одночасно оновлює UK locales і створює en.mdx.

Для кожного target система спочатку записує temporary file. Якщо target уже існує, він тимчасово стає backup. Новий файл займає target path, але backups не видаляються до завершення всіх перевірок.

ts
async function atomicWriteAll(files) {
  const completed = [];
  try {
    for (const file of files) {
      completed.push(await atomicWrite(file));
    }
    await nativeValidation();
    for (const item of completed) await item.commit();
  } catch (error) {
    for (const item of completed.reverse()) await item.rollback();
    throw error;
  }
}

Якщо portable contract, translation parity або native site validation падає, completed writes відкочуються у reverse order. Сайт не повинен залишитися з новим EN і старим UK або з locales: [uk, en] у файлі, для якого sibling не створився.

Atomic filesystem write не означає automatic publication. Він лише гарантує узгоджений локальний стан двох drafts.

Site delivery — окрема система доказів

У site repository MDX проходить lint, typecheck, tests, production build і visual preview. Перевіряються обидві locale routes, category, tags, mobile layout та shareable action-filter URLs.

Лише після цього draft змінюється на false. Branch, commit, push і draft PR виконуються як окремі контрольовані кроки. Після merge потрібний production deployment, а після deployment — smoke на реальних URLs.

Delivery evidence не додається в погоджений Digest, бо це змінило б його hash. Для цього існує окремий publication record: URLs, PR, commit, deployment і smoke result не змішуються з editorial source.

Таке розділення дозволяє чесно сказати, де саме знаходиться artifact. «Approved», «exported», «merged» і «published» — різні стани, а не синоніми слова «готово».

Revision замість переписування історії

Після першої публікації W33 ми виявили, що Digest потрібно доповнити Signals, які належали до того самого періоду. Просте редагування опублікованих Digest notes знищило б provenance попереднього approval.

Так з’явився Revision flow. R2 або R3 зберігає:

  • base Digest path;
  • base approval hash;
  • base Signal IDs;
  • added Signal IDs;
  • revised UK та EN paths;
  • нові approval й export states.

W33 R2 почався з одного base Signal і додав ще п’ять. Фінальний artifact містив шість Signals. Оригінальні Digest notes не перезаписувалися, а revised UK і EN отримали власні hashes.

Закритий Weekly Cycle не відкривався повторно. Видалений Temporary Content не відновлювався. Revised bilingual export атомарно замінив публічні uk.mdx і en.mdx, після чого revision closure перевірив exported EN provenance й запросив окреме cleanup confirmation.

Final revision record зафіксував status: closed, temporaryContentPurged: true, п’ять видалених temporary files і timestamps export/closure.

Revision потрібна для матеріалу, який належав до вже опублікованого періоду. Новий тематично пов’язаний Signal зазвичай має піти до наступного Weekly Cycle і, за наявності confirmed evidence, сформувати Було → Стало.

Evidence snapshot без вигаданого benchmark

Перший production cycle підтвердив:

  • один Digest у двох мовах;
  • шість Signals у W33 R2;
  • окремі UK та EN approval hashes;
  • успішний atomic bilingual replacement;
  • publication record і production smoke;
  • 100% bilingual ratio для першого Digest.

Останній показник не є місячним KPI. Ціль «не менше 70% Digest двома мовами» потребує щонайменше місячного періоду, а не одного artifact.

Timestamps дозволяють показати кілька elapsed intervals:

  • initial EN created → bilingual export: 2m 06s;
  • R2 opened → revised UK generated: 27m 57s;
  • revised EN generated → atomic export: 16m 56s;
  • R2 opened → atomic export: 1h 32m 30s.

Це не active-work measurements. Інтервали містять невідомий час редагування, очікування, troubleshooting та перерв. У v0.1 немає preparationStartedAt, reviewCompletedAt і pause events. Тому KPI «підготовка й review ≤30 хв» для W33 чесно позначений як not measured.

Те саме правило стосується test counts. Acceptance snapshot фіксує 18 Weekly Digest і 8 MDX Export tests. Ранні spike documents містять менші counts для попередніх plugin versions. Їх не можна складати або подавати як один незмінний показник.

За що ми заплатили складністю

Переваги цієї конструкції практичні:

  • approval відповідає конкретному editorial state;
  • UK залишається source of truth для EN;
  • bilingual identity перевіряється машинно;
  • partial export не залишається на диску;
  • Git і deployment не приховані всередині Obsidian plugin;
  • revision зберігає ancestry замість переписування історії.

Недоліки теж відчутні:

  • кілька approval gates збільшують editorial latency;
  • зміна UK інвалідує downstream EN work;
  • multi-file rollback і native validation ускладнюють exporter;
  • manual draft: false і Git delivery потребують дисципліни;
  • без active-time telemetry неможливо підтвердити KPI ≤30 хв;
  • revision додає новий lifecycle замість простого file edit.

Це не найкоротший шлях від Signal до статті. Але він не дозволяє швидкості приховати, який саме текст погодила людина, чи збігаються локалі та чи справді artifact опинився у production.

У наступній частині ми розберемо дев’ять інтеграційних помилок першого production cycle: stale plugin state, змінені model identity fields, excerpt limits, UK/EN transaction failures, revision flow і Git conflicts. Саме ці помилки перетворили початковий happy path на contracts, описані в цій статті.


← Назад до блогу