UK/EN Digest, Approval Hashes, and Atomic MDX Export

Digest changed after approval; approve it again.
This message appeared when we tried to export a Digest that already had approval but had been edited afterward. The system did not attempt to decide whether the change was “small enough.” It stopped the export.
At first, this can look like unnecessary bureaucracy around a Markdown file. In practice, an approval hash answers a simple question: does the current editorial artifact match the state a person actually reviewed and approved?
A hash does not prove that the text is factually correct. It does not verify primary sources or replace a reviewer. Without it, however, approval becomes a vague memory: “I think I have already seen this version.” The risk grows with bilingual publishing. The Ukrainian text may change after the English adaptation is generated; tags or Recommended Actions may diverge; one MDX file may be written while validation of the other fails.
That is why an Engineering Radar Digest is not two independent Markdown files and not a “translate” button. It is an editorial transaction with a state machine, two approvals, identity parity, atomic export, and a separate site delivery gate.
In this article, “we” means the author and an AI coding agent. I defined editorial authority, publication boundaries, and acceptance criteria. The agent helped turn them into contracts, commands, rollback logic, tests, and documentation. As in the earlier parts, we used the MAE approach: every automation had to prove not only its happy path but also its behavior with stale state, validation failure, and partially completed operations.
Ranking does not determine what gets published
The weekly flow starts by creating a Cycle for an ISO week. The Radar builds its candidate selection in a defined order:
Critical
→ Saved
→ relevance
→ Primary authority
→ diversityIgnored, pending, or not-yet-analyzed Signals are not included automatically. The same applies to undated Signals: they require a separate manual command. Critical Signals may exceed the normal limit and do not consume the diversity quota.
These rules make triage repeatable, but ranking has no publication authority. A person opens the Weekly Cycle, reviews membership, adds anything that was missed, and only then moves the selection from proposed to confirmed.
Once confirmed, membership cannot change silently. If the set changes, the reviewer must see the new state before Digest generation. This is the boundary between a recommendation engine and an editorial decision: the algorithm proposes a candidate set; a person is accountable for what becomes public material.
The Ukrainian Digest is the editorial source
Ukrainian generation is allowed only for a confirmed selection. In the recorded v0.1 configuration, Weekly Digest synthesis used the OpenAI Responses API, store: false, a strict schema, and gpt-5.6-terra as the default model.
The model receives structured Signal facts, source URLs, and current Recommended Actions. It is not supposed to begin a new research phase or introduce facts absent from the Signals. The result becomes draft-uk, not a publication.
confirmed weekly selection
↓
draft-uk
↓ human editorial review
approved-uk
↓ export
exported-ukThe reviewer checks the title, introduction, every Signal card, the engineering-radar category, tags, Recommended Actions, source links, and the Before → After sections.
That last element has its own evidence boundary. The Radar can compare thematically related Signals, but a public Before → After statement is allowed only when a confirmed published evolution record exists. Thematic similarity alone is not proof of evolution.
After editorial changes, the Ukrainian Digest moves to approval. This exact artifact becomes the source of truth for the English adaptation.
What the approval hash binds
The system builds a canonical Digest before hashing it. The input contains editorial frontmatter and the normalized Markdown body. Operational fields changed by the workflow must not change content identity, so approvalHash, export, and mutable status are excluded from the canonical input.
In abbreviated form:
canonical = editorialFrontmatter + normalize(body)
approvalHash = sha256(canonical)
if (sha256(currentCanonical) !== approvalHash) {
throw new Error("Digest changed after approval")
}On approval, SHA-256 is written to frontmatter. Before export, the system calculates it again. If the title, Signal membership, tags, actions, or body changed, the hashes no longer match and export is blocked.
In W33 R2, the Ukrainian and English Digests had separate current approval hashes. Truncated forms such as c921… and 827d… are enough for a public explanation. The values matter not as secrets or memorable identifiers, but as links to specific editorial states.
The hash does not say that the reviewer made the right decision. It says that the system is exporting the exact state to which that decision was applied.
The English adaptation is bound to the UK state
The English Digest is generated from an approved or already exported Ukrainian Digest and the same structured Signals. It preserves membership and ordering, source URLs, tags, Recommended Actions, and identity metadata.
The model may produce idiomatic English, but it cannot change the factual composition of the material. Identity fields are forcibly inherited from Ukrainian rather than accepted as free-form model output.
EN frontmatter contains sourceDigestId, sourceLanguage: uk, and sourceApprovalHash. In other words, the English artifact is derived not from an abstract “latest Ukrainian file,” but from a specifically approved Ukrainian state.
At the same time, English has its own lifecycle:
approved-uk
↓
draft-en
↓ human language and parity review
approved-en
↓ export
exported-enA separate English approval hash is necessary because the language adaptation is also an editorial artifact. The reviewer checks facts, tone, source URLs, order, actions, and whether the adaptation introduced any new information.
Independent English approval does not make English an independent source of truth. It confirms that the derived artifact was reviewed against a specific Ukrainian source state. If Ukrainian changes, the old English artifact is no longer current: Ukrainian must be approved again, and English must be regenerated and reviewed.
What identity parity requires
After bilingual export, uk.mdx and en.mdx share:
- slug;
- publication date;
- category
engineering-radar; - tags;
- Recommended Actions;
- cover source and dimensions;
locales: [uk, en].
The title, excerpt, cover alt, and body are localized. The remaining fields identify one publication in two languages.
This also protects taxonomy. The engineering-radar category is reserved exclusively for Digest publications and action filters. This implementation series belongs to engineering-architecture. The English model cannot accidentally turn a Digest into another content type or invent a new action name.
The exporter does not publish
The portable MDX contract validates a title of no more than 60 characters, an excerpt of no more than 160, taxonomy, actions, locales, the cover contract, and mandatory draft: true. It then runs the native loader of the external site repository.
The exporter runs no Git commands and never changes draft to false. This separation of authority is deliberate:
approved Digest
≠ exported MDX
≠ publishable MDX
≠ committed change
≠ merged change
≠ deployed articleThe Obsidian plugin may prepare a valid draft in the agreed structure. The decision to make it public belongs to a separate site delivery flow: local preview, quality gate, draft: false, Git branch, commit, PR, merge, deployment, and production smoke.
The chain is long because every link answers a different question. Approval proves content identity. Export proves contract compliance. Git records the change. Merge accepts it into the repository. Deployment delivers a build. Production smoke proves that a real route is available.
Part 3 supplied a practical reminder: its PR was merged, but a Vercel access rule blocked production deployment. Without a separate smoke check, we could have called the article published while its routes still returned 404.
Two MDX files must be one transaction
The first Ukrainian export creates uk.mdx with locales: [uk]. English export is allowed only after that. For the bilingual pair, it updates Ukrainian locales and creates en.mdx in one operation.
For every target, the system first writes a temporary file. If a target already exists, it temporarily becomes a backup. The new file takes the target path, but backups are retained until every validation finishes.
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;
}
}If the portable contract, translation parity, or native site validation fails, completed writes roll back in reverse order. The site must not be left with new English and old Ukrainian, or with locales: [uk, en] in a file whose sibling was never created.
An atomic filesystem write is not automatic publication. It guarantees only a consistent local state for both drafts.
Site delivery is a separate evidence system
In the site repository, MDX passes lint, typecheck, tests, a production build, and visual preview. Both locale routes, category, tags, mobile layout, and shareable action-filter URLs are checked.
Only then is draft changed to false. Branch, commit, push, and draft PR remain separate controlled steps. Merge must be followed by production deployment, and deployment by a smoke check against real URLs.
Delivery evidence is not appended to the approved Digest because that would change its hash. A separate publication record stores URLs, PR, commit, deployment, and smoke result without mixing them into the editorial source.
This separation makes the artifact state explicit. “Approved,” “exported,” “merged,” and “published” are different states, not synonyms for “done.”
Revision instead of rewriting history
After the first W33 publication, we found Signals from the same period that needed to be added. Editing the already published Digest notes in place would have destroyed the provenance of the earlier approval.
That led to the Revision flow. R2 or R3 records:
- base Digest path;
- base approval hash;
- base Signal IDs;
- added Signal IDs;
- revised Ukrainian and English paths;
- new approval and export states.
W33 R2 started with one base Signal and added five more. The final artifact contained six Signals. The original Digest notes were not overwritten; revised Ukrainian and English artifacts received their own hashes.
The closed Weekly Cycle was not reopened. Deleted Temporary Content was not restored. The revised bilingual export atomically replaced public uk.mdx and en.mdx. Revision closure then verified exported English provenance and requested a separate cleanup confirmation.
The final revision record stored status: closed, temporaryContentPurged: true, five deleted temporary files, and export and closure timestamps.
A Revision is appropriate for material that belonged to an already published period. A new thematically related Signal should normally enter the next Weekly Cycle and, when confirmed evidence exists, may produce a Before → After comparison.
An evidence snapshot without an invented benchmark
The first production cycle confirmed:
- one Digest in two languages;
- six Signals in W33 R2;
- separate Ukrainian and English approval hashes;
- successful atomic bilingual replacement;
- a publication record and production smoke;
- a 100% bilingual ratio for the first Digest.
The last figure is not a monthly KPI. The target of publishing at least 70% of Digests in two languages requires at least a monthly measurement period, not one artifact.
The timestamps provide several elapsed intervals:
- initial English created → bilingual export: 2m 06s;
- R2 opened → revised Ukrainian generated: 27m 57s;
- revised English generated → atomic export: 16m 56s;
- R2 opened → atomic export: 1h 32m 30s.
These are not active-work measurements. They include unknown editing, waiting, troubleshooting, and interruption time. v0.1 has no preparationStartedAt, reviewCompletedAt, or pause events. The KPI “preparation and review within 30 minutes” is therefore honestly marked not measured for W33.
The same rule applies to test counts. The acceptance snapshot records 18 Weekly Digest and 8 MDX Export tests. Early spike documents contain lower counts from earlier plugin versions. They cannot be added together or presented as one immutable measure.
What we paid for this control
The practical advantages are clear:
- approval refers to a specific editorial state;
- Ukrainian remains the source of truth for English;
- bilingual identity is machine-checked;
- partial exports do not remain on disk;
- Git and deployment are not hidden inside the Obsidian plugin;
- a Revision preserves ancestry instead of rewriting history.
The disadvantages are equally real:
- multiple approval gates increase editorial latency;
- a Ukrainian change invalidates downstream English work;
- multi-file rollback and native validation complicate the exporter;
- manual
draft: falseand Git delivery require discipline; - the ≤30-minute KPI cannot be verified without active-time telemetry;
- a Revision adds a new lifecycle instead of allowing a simple file edit.
This is not the shortest path from Signal to article. It does prevent speed from hiding which text a person approved, whether locales agree, and whether the artifact actually reached production.
The next article examines nine integration failures from the first production cycle: stale plugin state, changed model identity fields, excerpt limits, UK/EN transaction failures, the Revision flow, and Git conflicts. Those failures turned the original happy path into the contracts described here.
← Back to the blog