Skip to content

feat!: provenance becomes an ordinary module on a run seam - #207

Merged
SearheiParkhamchuk merged 1 commit into
major/translatorfrom
feat/translator-provenance-as-a-module
Oct 23, 2026
Merged

SearheiParkhamchuk merged 1 commit into
major/translatorfrom
feat/translator-provenance-as-a-module

Conversation

@SearheiParkhamchuk

Copy link
Copy Markdown
Contributor

Closes #198

The problem

The translation handler knew provenance by name. Four blocks guarded with provenance?. resolved the service, built its key, captured fingerprints, computed drift and wrote the receipt. Anything else that wanted to join a translation run — a glossary that must read from the CMS before the request, an audit entry after the save — had nowhere to go without editing that handler again.

What changes

A run has two positions any module can occupy: before the translation, and after the document is saved. Provenance is the first to take them, with no privileges.

  • A module returns only its contribution, not the whole run state. The chain merges moduleState by key, so a module cannot drop a slot it does not own.
  • A module that fails contributes nothing — the chain keeps the state that module was given, and a failure raised deliberately inside an open transaction is still let out rather than demoted to a log line.
  • Module names are refused at boot when two collide, because a name keys the module's slot in the run's state.

The config-time half follows. levels is replaced by modules and components, which separates what wires server routes from what attaches an admin control. The document and collection levels registered the same route bundle — the only reason endpoints were deduplicated at all — and once the controls moved out, the two were indistinguishable and collapsed into one module.

Breaking

The levels option is removed, with documentLevel(), collectionLevel(), fieldLevel() and the TranslationLevel type.

Was Becomes
levels: [documentLevel()] modules: [documentTranslation()] · components: [translateDocumentControl()]
levels: [collectionLevel()] modules: [documentTranslation()] · components: [bulkTranslationDashboard()]
levels: [fieldLevel()] modules: [fieldTranslation()]
omitted (the old default) modules: [documentTranslation()] · components: [translateDocumentControl(), bulkTranslationDashboard()]

Both lists are required and either may be []. An omitted option used to mean two surfaces silently; it now fails at boot with the table above in the message. Full entry in docs/DEPRECATIONS.md.

One behaviour change, worth seeing before it ships

An address absent from a stored receipt now means "translate" rather than "cannot tell". A field whose source was emptied and later refilled is re-translated instead of being left alone forever.

The consequence: a receipt that is a whole-document digest, or no receipt at all, reads as "every field changed". A document translated before provenance was switched on therefore gets every field re-translated on its first drift-only run — including text a person typed.

Also here

Route factories and the shared config context no longer accept the absence of an access guard. The guarantee is pinned with @ts-expect-error, so a route that forgets one fails the type-check rather than quietly serving unguarded.

Verification

  • 183 test files / 2445 checks green; apps/cms and apps/dev type-check clean
  • the real declaration build green — tsgo alone does not catch what it catches
  • integration: 63 files, 236 passed / 8 skipped, including two runs across a stored receipt for the drift cases

Three defects were found by review and confirmed by mutation before being fixed — in each case the whole suite stayed green over broken code:

Broken on purpose Suite said
dropped the fallback that carries the leaf-change map past a module contributing only its own note all 2443 checks passed
made a failed module reset the run to its opening state, wiping an earlier module's slot all 2443 checks passed
broke the import path in the unexport type pins, so @ts-expect-error was satisfied by "cannot find module" 0 type errors

Each now has a check that reddens on that mutation alone.

Not solved here

  • A run that overwrites a person's text still says nothing about it.
  • The field-level route writes no receipt at all.
  • A run that answered nothing still writes a fresh translatedAt.
  • The position after the translation and before the save has no tenant yet, so it is not built.

The translation handler wired provenance in by name: four blocks guarded with
`provenance?.` resolved the service, built its key, captured fingerprints,
computed drift and wrote the receipt. Nothing else could join a translation run
without editing that handler again.

A run now has two positions any module can occupy — before the translation, and
after the document is saved. Provenance is the first to take them, with no
privileges. A module returns only its contribution rather than the whole run
state; the chain merges `moduleState` by key, so a module cannot drop a slot it
does not own, and a module that fails contributes nothing instead of ending the
run. Module names are refused at boot when two collide, because a name keys the
module's slot in that state.

The config-time half follows. `levels` is replaced by `modules` and
`components`, separating what wires server routes from what attaches an admin
control. The document and collection levels registered the same route bundle —
the only reason endpoints were deduplicated at all — and once the controls moved
out they were indistinguishable and collapsed into one module.

Two further changes ride along:

- An address absent from a stored receipt now means "translate" rather than
  "cannot tell". A field whose source was emptied and later refilled is
  re-translated instead of being left alone forever.
- Route factories and the shared config context no longer accept the absence of
  an access guard. The guarantee is pinned with `@ts-expect-error`, so a route
  that forgets one fails the type-check rather than serving unguarded.

Closes #198

BREAKING CHANGE: the `levels` plugin option is removed, along with
`documentLevel()`, `collectionLevel()`, `fieldLevel()` and the
`TranslationLevel` type. Replace `levels: [documentLevel(), collectionLevel()]`
with `modules: [documentTranslation()]` and
`components: [translateDocumentControl(), bulkTranslationDashboard()]`;
`levels: [fieldLevel()]` becomes `modules: [fieldTranslation()]`. Both lists are
required and either may be empty — an omitted option used to mean two surfaces
silently, and now fails at boot with the replacement in the message.
See docs/DEPRECATIONS.md for the full table.
@vercel

vercel Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
ideal-cms Ready Ready Preview Oct 23, 2026 1:11pm UTC

Request Review

@SearheiParkhamchuk
SearheiParkhamchuk merged commit 02ef53a into major/translator Oct 23, 2026
2 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.0.0-next.5 🎉

The release is available on npm package (@next dist-tag)

Your semantic-release bot 📦🚀

This branch was successfully deployed

1 active deployment
Preview — 17f9c162 Deployed Oct 23, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[translator] Module extraction: provenance becomes an ordinary tenant

1 participant