Skip to content
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
262 commits
Select commit Hold shift + click to select a range
d4d03da
fix(i18n): enforce canonical translation resource identity
seonghobae Sep 3, 2026
0e30753
docs(adr): align DB and cache resource identities
seonghobae Sep 3, 2026
c60693d
test(i18n): require database-owned publication timestamp
seonghobae Sep 3, 2026
5973bbb
fix(i18n): make publication timestamp database-owned
seonghobae Sep 3, 2026
4d06580
docs(adr): make publication provenance database-owned
seonghobae Sep 3, 2026
d982d26
test(i18n): exercise translation ledger invariants in PostgreSQL
seonghobae Sep 3, 2026
74f0521
test(i18n): timestamp the publication transition itself
seonghobae Sep 3, 2026
e2429b1
fix(i18n): timestamp the publication transition statement
seonghobae Sep 3, 2026
3a3f809
test(i18n): align publication receipt with statement time
seonghobae Sep 3, 2026
0089692
docs(adr): bind publication evidence to the transition statement
seonghobae Sep 3, 2026
527abd6
test(i18n): reject padded translation identities
seonghobae Sep 3, 2026
66ff153
fix(i18n): reject padded translation identities
seonghobae Sep 3, 2026
4df91d9
docs(adr): align translation identity admission evidence
seonghobae Sep 3, 2026
a0a7cf4
test(i18n): forbid new psycopg2 verification reachability
seonghobae Sep 3, 2026
2ec9873
test(i18n): use asyncpg for ledger PostgreSQL verification
seonghobae Sep 3, 2026
8b01c7c
fix(i18n): rollback failed asyncpg publication transaction
seonghobae Sep 3, 2026
e325392
test(i18n): prove cache reads release DB lease
seonghobae Sep 3, 2026
0241549
fix(i18n): release DB lease before cache I/O
seonghobae Sep 3, 2026
f077d19
test(i18n): align miss-path lease expectations
seonghobae Sep 3, 2026
b4056e2
docs(i18n): record cache lease boundary
seonghobae Sep 3, 2026
aa050f1
test(i18n): RED immutable translation aggregate identity
seonghobae Sep 3, 2026
04c851f
fix(i18n): freeze translation aggregate identity after insert
seonghobae Sep 3, 2026
9b2720c
docs(adr): record immutable translation aggregate identity
seonghobae Sep 3, 2026
c46634a
test(i18n): keep aggregate identity guard in hosted contract
seonghobae Sep 3, 2026
e2b5b5f
test(i18n): reject padded translation key identity
seonghobae Sep 3, 2026
413ea3b
fix(i18n): canonicalize required translation key identity
seonghobae Sep 3, 2026
913e3d1
test(i18n): preserve canonical required-key guard in hosted CI
seonghobae Sep 3, 2026
baa1986
docs(adr): make translation key identity canonical
seonghobae Sep 3, 2026
da3b9c5
test(i18n): reject non-space edge whitespace identity
seonghobae Sep 3, 2026
f74b7e2
fix(i18n): reject edge whitespace beyond plain spaces
seonghobae Sep 3, 2026
d8c3864
test(i18n): preserve edge-whitespace identity guard
seonghobae Sep 3, 2026
1da44d3
docs(adr): align PostgreSQL edge-whitespace semantics
seonghobae Sep 3, 2026
47c2c21
test(i18n): reject database whitespace-only copy
seonghobae Sep 3, 2026
55df3d8
test(i18n): prove whitespace-only copy publication fails
seonghobae Sep 3, 2026
9b42748
fix(i18n): reject whitespace-only translation copy
seonghobae Sep 3, 2026
b03c91d
docs(adr): align immutable copy completeness
seonghobae Sep 3, 2026
9f04f09
test(i18n): require immutable translation projections
seonghobae Sep 3, 2026
035fbc8
fix(i18n): freeze published translation projections
seonghobae Sep 3, 2026
a56458f
docs(adr): bind translation value-object immutability
seonghobae Sep 3, 2026
ea95121
test(i18n): reject poisoned exact-version cache copy
seonghobae Sep 3, 2026
3495598
fix(i18n): bind cache copy to PostgreSQL digests
seonghobae Sep 3, 2026
1e9d6f9
test(i18n): model PostgreSQL cache integrity digests
seonghobae Sep 3, 2026
dc5e374
test(i18n): verify PostgreSQL copy digests
seonghobae Sep 3, 2026
22251bc
docs(adr): bind Valkey copy to PostgreSQL evidence
seonghobae Sep 3, 2026
c34f94b
test(i18n): preserve cache digest admission contract
seonghobae Sep 3, 2026
888a6f1
test(i18n): expose locale-dependent Unicode whitespace admission
seonghobae Sep 3, 2026
2b921e0
fix(i18n): make whitespace admission collation independent
seonghobae Sep 3, 2026
f0ad89d
fix(i18n): pin application whitespace repertoire
seonghobae Sep 3, 2026
e33ad22
test(i18n): align hosted whitespace contract
seonghobae Sep 3, 2026
caec5b7
docs(adr): pin collation-independent whitespace contract
seonghobae Sep 3, 2026
5e71da3
test(i18n): bind explicit whitespace repertoire
seonghobae Sep 3, 2026
8588bcc
test(i18n): reject resource versions outside bigint
seonghobae Sep 3, 2026
11326a2
fix(i18n): align resource version with bigint identity
seonghobae Sep 3, 2026
7222994
test(i18n): reject non-UTF-8 cache copy
seonghobae Sep 3, 2026
ac175dc
fix(i18n): fail cache surrogate copy closed
seonghobae Sep 3, 2026
ab5270f
test(i18n): reject non-string translation identities
seonghobae Sep 3, 2026
42e0af0
fix(i18n): fail closed on malformed identity types
seonghobae Sep 3, 2026
dbe484b
test(i18n): reject float cache version identity
seonghobae Sep 3, 2026
a46bf61
fix(i18n): enforce integer cache version identity
seonghobae Sep 3, 2026
1edab5c
test(i18n): reject non-PostgreSQL text identities
seonghobae Sep 3, 2026
6679d12
fix(i18n): align identity strings with PostgreSQL text
seonghobae Sep 3, 2026
0206d6a
test: require translation ledger rollback recovery
seonghobae Sep 3, 2026
7d47074
fix: add translation ledger rollback recovery
seonghobae Sep 3, 2026
14dd852
test: forbid destructive translation ledger rollback
seonghobae Sep 3, 2026
4de0353
fix: guard translation rollback against data loss
seonghobae Sep 3, 2026
2ad7877
test(i18n): expose rollback insert race
seonghobae Sep 3, 2026
7b60ad4
fix(i18n): serialize rollback emptiness guard
seonghobae Sep 3, 2026
be42d9b
test(i18n): pin rollback lock ordering
seonghobae Sep 3, 2026
4f3b809
test(i18n): pin TranslationScreen alias immutability RED
seonghobae Sep 4, 2026
83e4c65
fix(i18n): make TranslationScreen own immutable copy
seonghobae Sep 4, 2026
0ce3422
test(i18n): reject mismatched translation cache identity
seonghobae Sep 4, 2026
718f86d
fix(i18n): bind translation value object to cache identity
seonghobae Sep 4, 2026
80e9eba
test: require replay-safe translation rollback
seonghobae Sep 4, 2026
99521ba
test: preserve rollback replay tolerance
seonghobae Sep 4, 2026
f6f3758
fix: make translation rollback replay-safe
seonghobae Sep 4, 2026
0d3d858
test(i18n): reject invalid TranslationScreen projections
seonghobae Sep 4, 2026
223aca5
fix(i18n): enforce TranslationScreen projection invariants
seonghobae Sep 4, 2026
2e83785
fix(test): restore TranslationScreen regression syntax
seonghobae Sep 4, 2026
906f7e1
docs(gaps): refresh translation delivery evidence
Sep 4, 2026
ceed87e
feat(i18n): expose authenticated screen translations
Sep 4, 2026
249b59e
docs(gaps): record authenticated translation slice
Sep 4, 2026
cbc18ce
test(i18n): reject ambiguous duplicate cache members
seonghobae Sep 4, 2026
f516869
fix(i18n): reject duplicate cache JSON members
seonghobae Sep 4, 2026
503da98
test(i18n): keep translation API fixture off psycopg2
seonghobae Sep 4, 2026
f0924b7
test(i18n): verify API read through asyncpg boundary
seonghobae Sep 4, 2026
04ef4fc
fix(i18n): keep API integration on asyncpg
seonghobae Sep 4, 2026
494c005
test(i18n): pin rollback locale-preference admission
seonghobae Sep 4, 2026
e23c21d
fix(i18n): guard rollback locale preferences
seonghobae Sep 4, 2026
1b252eb
test(i18n): assert rollback locale lock ordering
seonghobae Sep 4, 2026
686e481
test(i18n): reject recursive cache payloads
seonghobae Sep 4, 2026
6e3129e
test(i18n): force decoder recursion RED
seonghobae Sep 4, 2026
c44cf49
fix(i18n): treat recursive cache JSON as miss
seonghobae Sep 4, 2026
bf37d39
test(i18n): prove recursive cache fixture
seonghobae Sep 4, 2026
b0d764b
test(i18n): pin published truncate immutability RED
seonghobae Sep 4, 2026
d9cc274
fix(i18n): guard published ledger from truncate
seonghobae Sep 4, 2026
bf04fbf
chore(i18n): add replay-safe truncate guard rollback
seonghobae Sep 4, 2026
77554c1
test(i18n): verify truncate guard migration behavior
seonghobae Sep 4, 2026
b481967
test(i18n): cover truncate guard admission and rollback
seonghobae Sep 4, 2026
d08e27f
fix(i18n): converge 0246 rollback after truncate guard
seonghobae Sep 4, 2026
aca158d
test(i18n): pin rollback convergence for truncate guard
seonghobae Sep 4, 2026
af049ae
test(i18n): require non-vacuous translation HTTP contract
seonghobae Sep 4, 2026
70d3436
test(i18n): exercise authenticated translation HTTP route
seonghobae Sep 4, 2026
9e4df93
test(i18n): align immutable cache fixture evidence
Sep 4, 2026
c53d055
test(i18n): pin gap baseline to live API slice
seonghobae Sep 4, 2026
fe8693b
docs(i18n): restore code-current gap baseline
seonghobae Sep 4, 2026
063a6d9
test(i18n): bound optional cache I/O
seonghobae Sep 4, 2026
12b8f75
fix(i18n): bound optional cache latency
seonghobae Sep 4, 2026
5e8f433
docs(i18n): record bounded cache failure path
seonghobae Sep 4, 2026
9604251
test(i18n): serialize publish with child truncate
seonghobae Sep 4, 2026
6db4b2a
fix(i18n): serialize publish with truncate guard
seonghobae Sep 4, 2026
85a9a93
test(i18n): pin truncate publication lock order
seonghobae Sep 4, 2026
3e10376
docs(i18n): record truncate publication serialization
seonghobae Sep 4, 2026
22788ad
test(i18n): reject truncate lock-order deadlock
seonghobae Sep 4, 2026
e618b51
test(i18n): pin nonblocking truncate admission
seonghobae Sep 4, 2026
f97145d
fix(i18n): fail truncate lock contention closed
seonghobae Sep 4, 2026
68baf19
docs(i18n): record nonblocking truncate admission
seonghobae Sep 4, 2026
bad21a9
test(i18n): align cache timeout helper call
Sep 4, 2026
f1877b8
docs(gaps): refresh exact-head delivery evidence
Sep 4, 2026
92850af
test(docs): pin draft and history formatting contracts
seonghobae Sep 4, 2026
d8d3e55
docs(product): restore draft evidence boundary
seonghobae Sep 4, 2026
834776c
docs(product): restore traceability after status repair
seonghobae Sep 4, 2026
7a8a648
docs(history): preserve raw archive behind lint-clean index
seonghobae Sep 4, 2026
e7509b4
test(docs): prove historical archive byte identity
seonghobae Sep 4, 2026
df3266f
fix(i18n): make translation rollback replay-safe
Sep 4, 2026
b941635
docs(gaps): refresh translation exact-head evidence
Sep 4, 2026
597528f
test(i18n): distinguish translation request validation failures
seonghobae Sep 4, 2026
f3524ad
refactor(i18n): type translation request validation failures
seonghobae Sep 4, 2026
84b7b5e
codex: address PR review feedback (#929)
Sep 4, 2026
4139858
fix(observability): replace deprecated log handler
Sep 4, 2026
fc53004
test(i18n): pin one-query exact-version cache miss RED
seonghobae Sep 4, 2026
a741c2f
docs(gaps): refresh exact-head verification evidence
Sep 4, 2026
a3c8e9d
perf(i18n): reuse exact-version PostgreSQL projection
seonghobae Sep 4, 2026
4ec808a
test(i18n): align immutable miss query budget
seonghobae Sep 4, 2026
655fee2
test(i18n): restore real recursion cache evidence
seonghobae Sep 4, 2026
3acd976
docs(gaps): align exact-version query-budget evidence
seonghobae Sep 4, 2026
ab2652b
test(i18n): align one-query cache fallbacks
Sep 4, 2026
c1e7bdf
test(i18n): preserve real recursion evidence
seonghobae Sep 4, 2026
e6041cf
docs(gaps): preserve executable recursion evidence
seonghobae Sep 4, 2026
ffa45c5
test(i18n): make recursion fallback version-independent
Sep 4, 2026
989e1f4
docs(gaps): align recursion portability evidence
Sep 4, 2026
265aa32
test(i18n): preserve real recursion wire evidence
seonghobae Sep 4, 2026
12284da
docs(gaps): converge dual recursion evidence
seonghobae Sep 4, 2026
e7ec66a
test(i18n): make wire recursion evidence portable
Sep 4, 2026
d6d9df6
test(i18n): RED pin real wire recursion evidence
seonghobae Sep 4, 2026
3ca4d77
fix(i18n): restore real decoder recursion proof
seonghobae Sep 4, 2026
65a7df7
docs(gaps): restore exact-head wire evidence boundary
seonghobae Sep 4, 2026
a6aecea
chore(main): non-force converge protected #930
seonghobae Sep 4, 2026
69a1bc4
chore(main): non-force converge protected #931
seonghobae Sep 4, 2026
2d730ae
test(i18n): make deep JSON cache evidence runtime-portable
Sep 4, 2026
7edbd4f
test(i18n): require actual deep JSON decoder exhaustion
seonghobae Sep 4, 2026
5df3120
fix(i18n): restore real decoder-exhaustion evidence
seonghobae Sep 4, 2026
ad83c4a
docs(gaps): restore strict translation wire evidence
seonghobae Sep 4, 2026
adf312b
test(i18n): preserve cache hit and miss query budgets
seonghobae Sep 4, 2026
15c344e
fix(i18n): preserve exact-version cache value
seonghobae Sep 4, 2026
49cc023
test(i18n): align corrupt-cache fallback budget
seonghobae Sep 4, 2026
e200e41
docs(adr): preserve cache-hit and miss economics
seonghobae Sep 4, 2026
49a0ee7
docs(gap): align translation cache query economics
seonghobae Sep 4, 2026
1de835d
test(i18n): align exact-head regression assertions
Sep 4, 2026
3e11cee
test(i18n): restore exact-head cache regressions
Sep 4, 2026
fbce5b1
docs(gap): refresh translation delivery boundary
Sep 4, 2026
e32098a
docs(gap): record adjacent exact-head evidence
Sep 4, 2026
26ced49
docs(gap): refresh PostgreSQL candidate head
Sep 4, 2026
722c6ed
docs(gap): reconcile TLS ADR collision
Sep 4, 2026
1d7d9a9
docs(gap): refresh TLS delivery state
Sep 4, 2026
42e1e2e
docs(gap): keep Customer Master acceptance state current
seonghobae Sep 4, 2026
cee066a
docs(gap): record current coordination queue
Sep 4, 2026
4c645a1
test(i18n): tighten current evidence alignment
seonghobae Sep 4, 2026
f70c7a7
docs(gap): adopt live postgres candidate head
seonghobae Sep 4, 2026
9a189fb
test(i18n): parse scoped baseline blockquote
Sep 4, 2026
953a569
test(i18n): tighten candidate evidence semantics
seonghobae Sep 4, 2026
3849803
docs(i18n): align current candidate evidence state
seonghobae Sep 4, 2026
481a53f
test(i18n): admit repaired candidate review state
seonghobae Sep 4, 2026
6042c37
docs(i18n): align ready validation boundary
seonghobae Sep 4, 2026
f07a755
docs(gaps): refresh live queue snapshot
seonghobae Sep 4, 2026
2a8ed5d
test(i18n): address review quality findings
Sep 5, 2026
0f4fd26
docs(gaps): refresh 2026-09-07 live queue snapshot
Sep 7, 2026
697531b
docs(gaps): refresh 2026-09-08 live queue snapshot
Sep 8, 2026
03f68c1
docs(gaps): record #983 coverage-scope repair evidence
Sep 8, 2026
f57eb32
docs(gaps): restore pending-check boundary
Sep 8, 2026
1551f9e
docs(gaps): record #983 CSF inventory execution evidence
Sep 8, 2026
b05dec6
docs(gaps): record #983 draft coverage progress
Sep 8, 2026
603a7e1
docs(gaps): record ontology export coverage on #983
Sep 8, 2026
fabdacd
docs: repair current translation validation snapshot
seonghobae Sep 8, 2026
1f0f705
docs(gaps): record 2026-09-09 exact-head fabdacd4 RCA and autonomous KPI
Sep 9, 2026
07388ae
docs(gaps): record 2026-09-09 seventh-cycle head 1f0f7059c RCA
Sep 9, 2026
aaad36a
docs(gaps): remove self-staling scheduler telemetry
seonghobae Sep 9, 2026
6c207f1
docs(gaps): preserve executable live-snapshot contract
seonghobae Sep 9, 2026
3c03ac3
docs(gaps): satisfy scoped adjacent-candidate contract
seonghobae Sep 9, 2026
e1da2c2
docs(gaps): derive PRD/TRD/UML/Gap/actions for #922 without volatile …
Sep 9, 2026
f389563
fix(i18n): keep admitted screens reachable and rollback safe
Sep 9, 2026
0889f49
test(i18n): align exact route and wire evidence contracts
Sep 9, 2026
3d17efb
fix(i18n): preserve canonical identities and replay safety
Sep 9, 2026
0db7d31
fix(i18n): bound cache decoding by authoritative copy
Sep 9, 2026
e48d529
fix(i18n): converge missing octet evidence to cache miss
Sep 9, 2026
d997a28
docs(gaps): record 2026-09-09 exact-head e48d52968 RCA
Sep 9, 2026
7244feb
docs(gaps): refresh d997a282f RCA with Strix PASS and noema gateway s…
Sep 10, 2026
506caa9
test(i18n): reproduce UTF-8 cache bound miss
seonghobae Sep 11, 2026
f898399
fix(i18n): measure UTF-8 cache bound in bytes
seonghobae Sep 11, 2026
eb6eca9
test(i18n): require complete Customer Master ledger seed
seonghobae Sep 17, 2026
0426a70
feat(i18n): seed Customer Master eight-locale review draft
seonghobae Sep 17, 2026
64fa200
test(i18n): recover after expected rollback refusal
seonghobae Sep 17, 2026
ed23e14
test: reproduce unowned Customer Master draft adoption
seonghobae Sep 17, 2026
a7028ec
fix: reserve Customer Master seed ownership before migration 0248
seonghobae Sep 17, 2026
feced08
fix: make Customer Master seed ownership rollback fail closed
seonghobae Sep 17, 2026
c00bb04
test: enforce Customer Master seed ownership guard ordering
seonghobae Sep 17, 2026
74eeb3c
test: cover owned Customer Master seed replay and rollback
seonghobae Sep 17, 2026
bd96d0e
fix: stamp migration sessions with exact source file
seonghobae Sep 17, 2026
99d6ec2
fix: bind Customer Master ownership to exact migration context
seonghobae Sep 17, 2026
200eaac
test: bind Customer Master seed to migration-runner provenance
seonghobae Sep 17, 2026
735245d
fix: preserve future seed ownership records on rollback
seonghobae Sep 17, 2026
911e3d6
fix: scope Customer Master ownership to migration 0248 lifecycle
seonghobae Sep 17, 2026
ff7d6c2
fix: stamp Customer Master rollback ownership context
seonghobae Sep 17, 2026
2cd9cf1
fix: release empty Customer Master ownership reservations safely
seonghobae Sep 17, 2026
86550c4
test: preserve operator lifecycle outside Customer Master seed context
seonghobae Sep 17, 2026
2b7951a
test(i18n): reproduce pg_temp ownership shadowing
seonghobae Sep 17, 2026
92cf139
fix(i18n): pin translation ownership trigger search path
seonghobae Sep 17, 2026
0ea8896
test: reproduce translation ledger pg_temp shadowing
seonghobae Sep 17, 2026
ff1084b
fix: pin translation ledger guard search path
seonghobae Sep 17, 2026
ac60da4
fix: pin truncate guard search path
seonghobae Sep 17, 2026
45f11ea
test: reproduce concurrent seed ownership initializer race
seonghobae Sep 17, 2026
4fdb294
fix: converge concurrent customer master seed reservations
seonghobae Sep 17, 2026
1becd50
test(i18n): reproduce concurrent Customer Master seed replay
seonghobae Sep 17, 2026
cb3d426
fix(i18n): serialize concurrent Customer Master seed creation
seonghobae Sep 17, 2026
f467cae
test(i18n): preserve reviewed Customer Master copy on replay
seonghobae Sep 17, 2026
4630664
fix(i18n): preserve reviewed Customer Master copy on seed replay
seonghobae Sep 17, 2026
4337d72
fix(i18n): stop replaying completed Customer Master seed
seonghobae Sep 17, 2026
c40170b
test(i18n): cover completed-seed startup admission
seonghobae Sep 17, 2026
0c03493
test: pin one-time translation seed retirement
seonghobae Sep 17, 2026
45d7a5e
fix: retire completed customer translation seed
seonghobae Sep 17, 2026
c52e7ba
test: pin retired seed constraint upgrade
seonghobae Sep 17, 2026
ca11b27
fix: upgrade retired seed ownership constraints
seonghobae Sep 17, 2026
db5435d
test(i18n): preserve operator ownership checks on upgrade
seonghobae Sep 17, 2026
4b6d21c
fix(i18n): preserve operator ownership constraints
seonghobae Sep 17, 2026
8b39573
test(db): reproduce partial retirement constraint upgrade
seonghobae Sep 17, 2026
667aca9
fix(db): version Customer Master ownership constraints
seonghobae Sep 17, 2026
d4f42f5
test(db): pin versioned retirement constraint replay
seonghobae Sep 17, 2026
92ac572
test(i18n): keep retired Customer Master seed authority
seonghobae Sep 24, 2026
314be83
fix(i18n): preserve retired seed history on rollback
seonghobae Sep 24, 2026
793db31
test(i18n): expose unguarded Customer Master seed evidence path
seonghobae Sep 24, 2026
e74d6e8
test(i18n): make seed-path RED fail on missing ownership evidence
seonghobae Sep 24, 2026
156bd71
test(i18n): validate Customer Master seed through production ownershi…
seonghobae Sep 24, 2026
2a87b29
test(i18n): preserve retired seed receipt across truncate
seonghobae Sep 24, 2026
527d7e1
fix(i18n): make seed provenance truncate-proof
seonghobae Sep 24, 2026
84a7b1f
test(i18n): preserve seed truncate guard across failed rollback
seonghobae Sep 24, 2026
787aac2
fix(i18n): keep seed truncate guard through rollback validation
seonghobae Sep 24, 2026
1219fe4
fix(i18n): remove truncate guard atomically with ownership table
seonghobae Sep 24, 2026
b3df385
test(i18n): expose seed replay delete lock inversion
seonghobae Sep 24, 2026
8153ece
fix(i18n): remove seed replay root lock inversion
seonghobae Sep 24, 2026
44c6ccc
test(i18n): require explicit Customer Master resolution action copy
seonghobae Sep 24, 2026
f076fa4
fix(i18n): name Customer Master organization-identification action
seonghobae Sep 24, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
276 changes: 276 additions & 0 deletions backend/app/translation_ledger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
"""Versioned product-UI translation reads with exact screen-key cache identities.

PostgreSQL is the source of truth. Valkey is only an exact-version read cache;
malformed or unavailable cache data falls back to PostgreSQL and can never
supply ontology labels or cross-locale fallback copy.
"""

from __future__ import annotations

import json
from collections.abc import Mapping, Sequence
from dataclasses import dataclass
from typing import Protocol

import asyncpg
from redis.exceptions import RedisError


SUPPORTED_UI_LOCALES: tuple[str, ...] = ("ko", "en", "ja", "zh", "vi", "es", "de", "fr")
_CACHE_TTL_SECONDS = 300

_SELECT_SCREEN_SQL = """
with selected_resource as (
select resource_id, resource_version
from ui_translation_resource
where product_key = $1
and screen_key = $2
and publication_state = 'published'
and ($4::bigint is null or resource_version = $4)
order by resource_version desc
limit 1
)
select selected_resource.resource_version,
translation_key.translation_key,
translation_text.translated_text
from selected_resource
join ui_translation_key as translation_key
on translation_key.resource_id = selected_resource.resource_id
left join ui_translation_text as translation_text
on translation_text.resource_id = translation_key.resource_id
and translation_text.translation_key = translation_key.translation_key
and translation_text.locale = $3
order by translation_key.translation_key
"""


class TranslationCoverageError(RuntimeError):
"""Raised when a requested locale lacks any key required by a screen."""


class TranslationResourceNotFound(LookupError):
"""Raised when no published resource exists for the requested identity."""


class AsyncTranslationCache(Protocol):
"""Minimal Valkey-compatible contract used by the translation read model."""

async def get(self, key: str) -> str | bytes | None:
"""Return a cached payload or ``None`` when the key is absent."""
...
Comment thread
seonghobae marked this conversation as resolved.

async def set(self, key: str, value: str, *, ex: int) -> object:
"""Store a payload with a bounded TTL."""
...
Comment thread
seonghobae marked this conversation as resolved.


@dataclass(frozen=True, slots=True)
class TranslationScreen:
"""One immutable, complete product-screen translation projection."""

product_key: str
screen_key: str
resource_version: int
locale: str
cache_key: str
translations: dict[str, str]


def validate_ui_locale(locale: str) -> str:
"""Return a supported locale or reject it without fallback substitution."""
if locale not in SUPPORTED_UI_LOCALES:
raise ValueError(f"unsupported UI locale: {locale!r}")
return locale


def _validate_identity_segment(value: str, *, field_name: str) -> str:
"""Reject blank or delimiter-bearing cache identity segments."""
normalized = value.strip()
if not normalized or ":" in normalized:
raise ValueError(f"{field_name} must be nonblank and must not contain ':'")
return normalized


def build_translation_cache_key(
product_key: str,
screen_key: str,
resource_version: int,
locale: str,
) -> str:
"""Bind one cache entry to product, screen, immutable version, and locale."""
product = _validate_identity_segment(product_key, field_name="product_key")
screen = _validate_identity_segment(screen_key, field_name="screen_key")
if isinstance(resource_version, bool) or not isinstance(resource_version, int) or resource_version <= 0:
raise ValueError("resource_version must be a positive integer")
language = validate_ui_locale(locale)
return f"ui-translation:{product}:{screen}:v{resource_version}:{language}"


def require_complete_translation_map(
required_keys: Sequence[str],
translations: Mapping[str, str | None],
*,
locale: str,
) -> dict[str, str]:
"""Return the exact screen projection or fail closed on missing/blank copy."""
validate_ui_locale(locale)
projection: dict[str, str] = {}
missing: list[str] = []
for key in required_keys:
value = translations.get(key)
if not isinstance(value, str) or not value.strip():
missing.append(key)
continue
projection[key] = value
if missing:
missing_keys = ", ".join(sorted(missing))
raise TranslationCoverageError(f"{locale} translation is incomplete: {missing_keys}")
return projection


def _decode_cached_screen(
raw_payload: str | bytes,
*,
product_key: str,
screen_key: str,
resource_version: int,
locale: str,
) -> TranslationScreen | None:
"""Accept a cache hit only when every identity field and copy value is valid."""
try:
decoded = json.loads(raw_payload)
except (json.JSONDecodeError, UnicodeDecodeError, TypeError):
return None
if not isinstance(decoded, dict):
return None
if decoded.get("product_key") != product_key or decoded.get("screen_key") != screen_key:
return None
if decoded.get("resource_version") != resource_version or decoded.get("locale") != locale:
return None
translations = decoded.get("translations")
if not isinstance(translations, dict) or not translations:
return None
if any(not isinstance(key, str) or not isinstance(value, str) or not value.strip() for key, value in translations.items()):
return None
cache_key = build_translation_cache_key(product_key, screen_key, resource_version, locale)
return TranslationScreen(
product_key=product_key,
screen_key=screen_key,
resource_version=resource_version,
locale=locale,
cache_key=cache_key,
translations=dict(translations),
)


async def _read_exact_cache(
cache: AsyncTranslationCache | None,
*,
product_key: str,
screen_key: str,
resource_version: int,
locale: str,
) -> TranslationScreen | None:
"""Read an exact-version cache entry, treating cache failure as a DB miss."""
if cache is None:
return None
cache_key = build_translation_cache_key(product_key, screen_key, resource_version, locale)
try:
raw_payload = await cache.get(cache_key)
except RedisError:
return None
if raw_payload is None:
return None
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
return _decode_cached_screen(
raw_payload,
product_key=product_key,
screen_key=screen_key,
resource_version=resource_version,
locale=locale,
)


async def _write_exact_cache(cache: AsyncTranslationCache | None, screen: TranslationScreen) -> None:
"""Populate the exact-version cache without making cache availability authoritative."""
if cache is None:
return
payload = json.dumps(
{
"product_key": screen.product_key,
"screen_key": screen.screen_key,
"resource_version": screen.resource_version,
"locale": screen.locale,
"translations": screen.translations,
},
ensure_ascii=False,
separators=(",", ":"),
sort_keys=True,
)
try:
await cache.set(screen.cache_key, payload, ex=_CACHE_TTL_SECONDS)
except RedisError:
return


async def read_translation_screen(
pool: asyncpg.Pool,
cache: AsyncTranslationCache | None,
*,
product_key: str,
screen_key: str,
locale: str,
resource_version: int | None = None,
) -> TranslationScreen:
"""Read one published screen version and reject incomplete requested-locale copy.

Explicit versions may be served from Valkey because their identity is immutable.
A latest-version read first resolves PostgreSQL so a stale cache alias can never
hide a newly published resource.
"""
product = _validate_identity_segment(product_key, field_name="product_key")
screen = _validate_identity_segment(screen_key, field_name="screen_key")
language = validate_ui_locale(locale)
if resource_version is not None:
cached = await _read_exact_cache(
cache,
product_key=product,
screen_key=screen,
resource_version=resource_version,
locale=language,
)
if cached is not None:
return cached
if isinstance(resource_version, bool) or not isinstance(resource_version, int) or resource_version <= 0:
raise ValueError("resource_version must be a positive integer")

async with pool.acquire() as connection:
rows = await connection.fetch(
_SELECT_SCREEN_SQL,
product,
screen,
language,
resource_version,
)
if not rows:
raise TranslationResourceNotFound(
f"no published translation resource for {product}/{screen} version {resource_version!r}"
)

resolved_version = int(rows[0]["resource_version"])
required_keys = [str(row["translation_key"]) for row in rows]
values = {
str(row["translation_key"]): row["translated_text"]
for row in rows
}
projection = require_complete_translation_map(required_keys, values, locale=language)
cache_key = build_translation_cache_key(product, screen, resolved_version, language)
result = TranslationScreen(
product_key=product,
screen_key=screen,
resource_version=resolved_version,
locale=language,
cache_key=cache_key,
translations=projection,
)
await _write_exact_cache(cache, result)
return result
81 changes: 81 additions & 0 deletions docs/adr/0362-versioned-ui-translation-ledger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# ADR 0362: Version product UI translations in PostgreSQL

- Status: Proposed
- Date: 2026-09-03
- Owners: LineageWeave product read model / Customer Master UI composition
- Related: #922, `migrations/0246_ui_translation_ledger.sql`, `backend/app/translation_ledger.py`

## Problem

LineageWeave currently ships product UI copy in the frontend bundle and admits only `en`, `ko`, `zh`, `ja`, and `vi` as persisted member locale preferences. That makes a deploy artifact the mutable source of product copy, leaves the required `es`, `de`, and `fr` buyer paths unsupported, and provides no immutable screen-version identity for evidence, rollback, or cache correctness.

This ledger is strictly for LineageWeave-owned product UI copy. Ontology labels, concept names, and semantic truth remain with their canonical owners and must enter LineageWeave only through released contracts or ACLs.

## Constraints

- The product locale contract is exactly `ko`, `en`, `ja`, `zh`, `vi`, `es`, `de`, and `fr` for this increment.
- Locale identifiers are language tags interpreted according to BCP 47 / RFC 5646; adding region or script distinctions requires an explicit product decision and a new compatible version of the contract.
- PostgreSQL is authoritative. Valkey is an optional read cache and must not become a second source of truth.
- A published screen version is immutable and complete for every required screen key in all eight locales.
- Reads do not silently fall back to another locale. Missing or blank requested-locale copy is an error.
- Cache identity must include product, screen, immutable resource version, and locale.
- Publication must serialize with child key/text mutation so a complete resource cannot become incomplete after the publication check.
- The design must stay independent from ontology-label persistence and from another CWL product's domain tables.

## Alternatives considered

### Keep translations in the SPA bundle

Rejected. It couples copy lifecycle to frontend deployment, cannot provide an immutable database identity for a screen/version/locale projection, and perpetuates the five-locale gap.

### Store product copy with ontology labels

Rejected. Product UI copy and semantic concept labels have different ownership, versioning, review, and rollback semantics. Sharing their source of truth would violate the canonical-owner boundary and make ordinary copy changes semantic changes.

### Use a mutable key/value translation table

Rejected. In-place mutation destroys the evidence needed to reproduce what a buyer saw and makes cache invalidation dependent on timing rather than identity.

### Version product-owned screen resources in PostgreSQL

Selected. It gives the read model a stable aggregate identity, keeps copy ownership local to LineageWeave, and permits exact-version caching without duplicating semantic truth.

## Decision

`ui_translation_resource` is the aggregate root identified by `(product_key, screen_key, resource_version)`. A resource starts as `draft`; publication is a one-way transition. `ui_translation_key` declares the screen's required keys. `ui_translation_text` supplies one nonblank value for each `(resource_id, translation_key, locale)`.

The schema remains in 3NF: resource version metadata, required keys, and localized values are separate relations. The database enforces unique resource versions and unique localized values. Publication rejects an empty key set or any missing member of the required key × eight-locale matrix. Once published, the root and all child rows are immutable.

Child insert/update/delete obtains a `FOR UPDATE` lock on the parent resource. Publication already locks the resource row through its update. Therefore publication and child mutation are serialized: either the child change commits before the completeness scan, or it observes the published state and is rejected. Child rows may not be re-parented between resources.

`read_translation_screen` returns a complete `TranslationScreen` projection. Latest-version reads resolve PostgreSQL first so a stale cache alias cannot hide a newer publication. Explicit immutable versions may be served by Valkey under `ui-translation:{product}:{screen}:v{resource_version}:{locale}`. Malformed, unavailable, or identity-mismatched cache entries are misses and fall back to PostgreSQL. An unavailable cache never makes a valid PostgreSQL translation unavailable.

The existing `user_account.preferred_locale` constraint expands to the same eight language tags. API request validation and frontend consumption must be cut over to the same contract before #922 can close; the database/read-model foundation alone is not buyer-visible completion.

## DDD mapping

- Subdomain: product composition / presentation read model.
- Bounded context: LineageWeave product read model.
- Aggregate: versioned UI translation resource.
- Entity/value identity: required screen key; locale-tagged translated text.
- Repository boundary: PostgreSQL query in `backend.app.translation_ledger`; Valkey is a cache adapter, not a repository of record.
- Invariants: exact eight-locale completeness at publication, immutable published versions, no cross-resource child move, no locale fallback, exact cache identity.
- ACL: ontology labels remain external semantic truth and are not stored in these tables.

## Recovery and migration

Migration 0246 is additive for translation resources and only broadens the existing member locale constraint. While the old SPA bundle is still the consumer, deploying the migration is backward-compatible. If application rollout fails before consumers switch, roll back the application path while retaining the additive schema and any draft resources.

Published translation data is not destructively down-migrated. A bad published resource is corrected by publishing a new `resource_version` and moving consumers to that version/latest publication. Once customer copy exists, rollback means application/read routing to a previously admitted version, not dropping tables or rewriting published rows.

## Evidence

- RED `092d32137fd6764a4f1fc7a53125a15318814292`: executable contract required the eight locales, normalized schema, exact cache identity, and fail-closed completeness before implementation existed.
- RED `370f83a28f4a0fdeb000ea5a259d66c415c1a746`: review found that child mutation could race publication; the contract required parent-row serialization.
- Repair `bb61753db7a483c766690e830700637694135208`: child mutation now locks the parent resource with `FOR UPDATE`, preserving completeness across concurrent publication.

These commits are branch evidence only. This ADR remains Proposed until the exact protected-line implementation and dependent API/frontend cutover are verified.

## References

Internet Engineering Task Force. (2009). *Tags for identifying languages (BCP 47 / RFC 5646)*. RFC Editor. https://www.rfc-editor.org/rfc/rfc5646.html
Loading
Loading