fix(tm):建立 Trusted 唯一性与 Supersede 治理

This commit is contained in:
2026-09-18 06:44:14 +08:00
parent e486f1aaaa
commit ff1adb91ee
37 changed files with 2679 additions and 530 deletions
+116 -4
View File
@@ -56,6 +56,7 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
"/admin/translation/handoff",
"/admin/translation/memory/summary",
"/admin/translation/memory/query",
"/admin/translation/memory/conflicts",
"/admin/translation/glossary/summary",
"/admin/translation/glossary/query",
"/admin/translation/glossary/diagnose",
@@ -78,6 +79,7 @@ func (s *Server) handleAdminIndex(w http.ResponseWriter, r *http.Request) {
"/admin/control/translation-worker-run",
"/admin/control/translation-proofread",
"/admin/control/translation-memory-confirm",
"/admin/control/translation-memory-resolve-conflict",
"/admin/control/translation-glossary-add",
"/admin/control/translation-glossary-update",
"/admin/control/translation-glossary-approve",
@@ -133,6 +135,10 @@ func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
s.handleAdminTranslationMemoryConfirm(w, r)
return
}
if action == "translation-memory-resolve-conflict" {
s.handleAdminTranslationMemoryResolveConflict(w, r)
return
}
if strings.HasPrefix(action, "translation-glossary-") {
s.handleAdminGlossaryControl(w, r, action)
return
@@ -222,6 +228,34 @@ func (s *Server) handleAdminControl(w http.ResponseWriter, r *http.Request) {
})
}
func (s *Server) handleAdminTranslationMemoryResolveConflict(w http.ResponseWriter, r *http.Request) {
backend, ok := s.backend.(TranslationMemoryBackend)
if !ok || backend == nil {
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_memory_backend_unavailable", "Rust bat Translation Memory backend is unavailable")
return
}
var params backendrpc.TranslationMemoryResolveConflictParams
if !decodeAdminTranslationJSON(w, r, &params) {
return
}
if err := validateTranslationMemoryResolveConflictParams(params); err != nil {
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_memory_params", err.Error())
return
}
result, err := backend.TranslationMemoryResolveConflict(r.Context(), params)
if err != nil {
s.writeControlBackendError(w, "translation-memory-resolve-conflict", err)
return
}
writeNoStoreJSON(w, http.StatusAccepted, AdminControlResponse{
Service: "bat-api",
Action: "translation-memory-resolve-conflict",
RPCMethod: "translation.memory.resolve_conflict",
Status: "accepted",
Result: result,
})
}
func (s *Server) handleAdminTranslationTaskUpdate(w http.ResponseWriter, r *http.Request) {
backend, ok := s.backend.(TranslationBackend)
if !ok || backend == nil {
@@ -1016,6 +1050,37 @@ func (s *Server) handleAdminTranslationMemoryQuery(w http.ResponseWriter, r *htt
writeNoStoreJSON(w, http.StatusOK, result)
}
func (s *Server) handleAdminTranslationMemoryConflicts(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
return
}
if !s.requireAdminToken(w, r) {
return
}
backend, ok := s.backend.(TranslationMemoryBackend)
if !ok || backend == nil {
writeErrorJSON(w, http.StatusServiceUnavailable, "translation_memory_backend_unavailable", "Rust bat Translation Memory backend is unavailable")
return
}
params, err := translationMemoryConflictsParams(r)
if err != nil {
writeErrorJSON(w, http.StatusBadRequest, "invalid_translation_memory_query", err.Error())
return
}
result, err := backend.TranslationMemoryConflicts(r.Context(), params)
if err != nil {
s.writeControlBackendError(w, "translation-memory-conflicts", err)
return
}
if r.Method == http.MethodHead {
w.Header().Set("Cache-Control", "no-store")
w.WriteHeader(http.StatusOK)
return
}
writeNoStoreJSON(w, http.StatusOK, result)
}
func (s *Server) handleAdminSchedules(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet && r.Method != http.MethodHead {
writeErrorJSON(w, http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed")
@@ -1139,6 +1204,20 @@ func translationMemoryQueryParams(r *http.Request) (backendrpc.TranslationMemory
return params, nil
}
func translationMemoryConflictsParams(r *http.Request) (backendrpc.TranslationMemoryConflictsParams, error) {
params := backendrpc.TranslationMemoryConflictsParams{
TranslationMemoryPath: firstTrimmedQuery(r.URL.Query(), "translation_memory_path", "tm_path"),
}
if raw := strings.TrimSpace(r.URL.Query().Get("limit")); raw != "" {
limit, err := strconv.ParseUint(raw, 10, 64)
if err != nil || limit == 0 || limit > 1000 {
return backendrpc.TranslationMemoryConflictsParams{}, errors.New("limit must be in 1..=1000")
}
params.Limit = &limit
}
return params, nil
}
func parseTextUnitQueryParams(r *http.Request) (backendrpc.TextUnitQueryParams, error) {
query := r.URL.Query()
params := backendrpc.TextUnitQueryParams{
@@ -1334,6 +1413,32 @@ func validateTranslationMemoryConfirmParams(params backendrpc.TranslationMemoryC
if strings.TrimSpace(params.RecordID) == "" || strings.TrimSpace(params.Reviewer) == "" {
return errors.New("translation memory confirm requires record_id and reviewer")
}
if strings.TrimSpace(params.SupersedeRecordID) != "" && strings.TrimSpace(params.Reason) == "" {
return errors.New("translation memory supersede requires reason")
}
return nil
}
func validateTranslationMemoryResolveConflictParams(params backendrpc.TranslationMemoryResolveConflictParams) error {
if strings.TrimSpace(params.WinnerRecordID) == "" ||
strings.TrimSpace(params.Reviewer) == "" ||
strings.TrimSpace(params.Reason) == "" {
return errors.New("translation memory conflict resolution requires winner_record_id, reviewer, and reason")
}
if len(params.ExpectedTrustedRecordIDs) == 0 {
return errors.New("translation memory conflict resolution requires expected_trusted_record_ids")
}
seen := make(map[string]struct{}, len(params.ExpectedTrustedRecordIDs))
for _, id := range params.ExpectedTrustedRecordIDs {
id = strings.TrimSpace(id)
if id == "" {
return errors.New("expected_trusted_record_ids cannot contain empty record IDs")
}
if _, ok := seen[id]; ok {
return errors.New("expected_trusted_record_ids must be unique")
}
seen[id] = struct{}{}
}
return nil
}
@@ -1412,10 +1517,17 @@ func (s *Server) writeControlBackendError(w http.ResponseWriter, action string,
message = "control request was canceled"
default:
var apiErr *backendrpc.APIError
if errors.As(err, &apiErr) && apiErr.Kind == "not_implemented" {
status = http.StatusNotImplemented
code = "control_not_implemented"
message = "Rust bat does not implement this control action"
if errors.As(err, &apiErr) {
switch apiErr.Kind {
case "not_implemented":
status = http.StatusNotImplemented
code = "control_not_implemented"
message = "Rust bat does not implement this control action"
case "rpc_invalid_params":
status = http.StatusBadRequest
code = "invalid_control_params"
message = apiErr.Message
}
}
}
s.logger.Printf("bat-api control action=%s error=%v", action, err)
+76 -24
View File
@@ -1023,22 +1023,24 @@ func (f *fakeBackend) TaskCancel(ctx context.Context, taskID string) (*backendrp
type controlBackend struct {
*fakeBackend
calls []string
parseTextUnitQueries []backendrpc.TextUnitQueryParams
parseErrorQueries []backendrpc.TextUnitQueryParams
translationTaskUpdates []backendrpc.TranslationTaskUpdateParams
translationTaskListParams []backendrpc.TranslationTaskListParams
translationMemorySummaryParams []backendrpc.TranslationMemorySummaryParams
translationMemoryQueryParams []backendrpc.TranslationMemoryQueryParams
translationMemoryConfirmParams []backendrpc.TranslationMemoryConfirmParams
glossarySummaryParams []backendrpc.GlossarySummaryParams
glossaryQueryParams []backendrpc.GlossaryQueryParams
glossaryDiagnoseParams []backendrpc.GlossaryDiagnoseParams
glossaryMutationParams []backendrpc.GlossaryTermMutationParams
glossaryReviewParams []backendrpc.GlossaryReviewParams
glossaryDeleteParams []backendrpc.GlossaryDeleteParams
localizedPublishParams []backendrpc.LocalizedPublishParams
localizedRollbackParams []backendrpc.LocalizedRollbackParams
calls []string
parseTextUnitQueries []backendrpc.TextUnitQueryParams
parseErrorQueries []backendrpc.TextUnitQueryParams
translationTaskUpdates []backendrpc.TranslationTaskUpdateParams
translationTaskListParams []backendrpc.TranslationTaskListParams
translationMemorySummaryParams []backendrpc.TranslationMemorySummaryParams
translationMemoryQueryParams []backendrpc.TranslationMemoryQueryParams
translationMemoryConfirmParams []backendrpc.TranslationMemoryConfirmParams
translationMemoryConflictsParams []backendrpc.TranslationMemoryConflictsParams
translationMemoryResolveParams []backendrpc.TranslationMemoryResolveConflictParams
glossarySummaryParams []backendrpc.GlossarySummaryParams
glossaryQueryParams []backendrpc.GlossaryQueryParams
glossaryDiagnoseParams []backendrpc.GlossaryDiagnoseParams
glossaryMutationParams []backendrpc.GlossaryTermMutationParams
glossaryReviewParams []backendrpc.GlossaryReviewParams
glossaryDeleteParams []backendrpc.GlossaryDeleteParams
localizedPublishParams []backendrpc.LocalizedPublishParams
localizedRollbackParams []backendrpc.LocalizedRollbackParams
}
func (b *controlBackend) DaemonReload(ctx context.Context) (*backendrpc.Ack, error) {
@@ -1183,18 +1185,19 @@ func (b *controlBackend) TranslationProofread(ctx context.Context) (json.RawMess
func (b *controlBackend) TranslationMemorySummary(ctx context.Context, params backendrpc.TranslationMemorySummaryParams) (*backendrpc.TranslationMemorySummaryReport, error) {
b.calls = append(b.calls, "translation.memory.summary")
b.translationMemorySummaryParams = append(b.translationMemorySummaryParams, params)
schemaVersion := uint64(1)
schemaVersion := uint64(2)
return &backendrpc.TranslationMemorySummaryReport{
Available: true,
Path: params.TranslationMemoryPath,
SchemaVersion: &schemaVersion,
Summary: &backendrpc.TranslationMemorySummary{
SchemaVersion: schemaVersion,
RecordCount: 2,
TrustedCount: 1,
CandidateCount: 1,
SupersededCount: 0,
RejectedCount: 0,
SchemaVersion: schemaVersion,
RecordCount: 2,
TrustedCount: 1,
CandidateCount: 1,
SupersededCount: 0,
RejectedCount: 0,
CurrentTrustedCount: 1,
},
}, nil
}
@@ -1225,10 +1228,33 @@ func (b *controlBackend) TranslationMemoryConfirm(ctx context.Context, params ba
}, nil
}
func (b *controlBackend) TranslationMemoryConflicts(ctx context.Context, params backendrpc.TranslationMemoryConflictsParams) (*backendrpc.TranslationMemoryConflictsReport, error) {
b.calls = append(b.calls, "translation.memory.conflicts")
b.translationMemoryConflictsParams = append(b.translationMemoryConflictsParams, params)
return &backendrpc.TranslationMemoryConflictsReport{
Available: true,
Path: params.TranslationMemoryPath,
Conflicts: []backendrpc.TranslationMemoryConflict{},
}, nil
}
func (b *controlBackend) TranslationMemoryResolveConflict(ctx context.Context, params backendrpc.TranslationMemoryResolveConflictParams) (*backendrpc.TranslationMemoryResolveConflictReport, error) {
b.calls = append(b.calls, "translation.memory.resolve_conflict")
b.translationMemoryResolveParams = append(b.translationMemoryResolveParams, params)
return &backendrpc.TranslationMemoryResolveConflictReport{
Available: true,
Path: params.TranslationMemoryPath,
Entry: backendrpc.TranslationMemoryEntry{
RecordID: params.WinnerRecordID,
TrustStatus: backendrpc.TranslationMemoryStatusTrusted,
},
}, nil
}
func (b *controlBackend) GlossarySummary(ctx context.Context, params backendrpc.GlossarySummaryParams) (*backendrpc.GlossarySummaryReport, error) {
b.calls = append(b.calls, "translation.glossary.summary")
b.glossarySummaryParams = append(b.glossarySummaryParams, params)
schemaVersion := uint64(1)
schemaVersion := uint64(2)
return &backendrpc.GlossarySummaryReport{
Available: true,
Path: params.GlossaryPath,
@@ -1525,6 +1551,18 @@ func TestAdminTranslationQueryEndpointsProxyAuthenticatedRequests(t *testing.T)
t.Fatalf("missing TM query source status=%d body=%s", recorder.Code, recorder.Body.String())
}
request = httptest.NewRequest(http.MethodGet, "/admin/translation/memory/conflicts?translation_memory_path=%2Fvar%2Flib%2Fbat%2Ftranslation-memory.sqlite&limit=7", nil)
request.Header.Set("Authorization", "Bearer translation-token")
recorder = httptest.NewRecorder()
s.Handler().ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK ||
len(backend.translationMemoryConflictsParams) != 1 ||
backend.translationMemoryConflictsParams[0].TranslationMemoryPath != "/var/lib/bat/translation-memory.sqlite" ||
backend.translationMemoryConflictsParams[0].Limit == nil ||
*backend.translationMemoryConflictsParams[0].Limit != 7 {
t.Fatalf("TM conflicts status=%d body=%s params=%#v", recorder.Code, recorder.Body.String(), backend.translationMemoryConflictsParams)
}
request = httptest.NewRequest(http.MethodGet, "/admin/translation/glossary/summary?glossary_path=%2Fvar%2Flib%2Fbat%2Fglossary.sqlite", nil)
request.Header.Set("Authorization", "Bearer translation-token")
recorder = httptest.NewRecorder()
@@ -2345,6 +2383,7 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
{name: "translation worker run", action: "translation-worker-run", body: `{"provider":"mock","concurrency":8,"max_tasks":2,"retry_backoff_seconds":0,"worker_id":"dashboard-worker"}`, rpcMethod: "translation.worker.run", call: "translation.worker.run"},
{name: "translation proofread", action: "translation-proofread", rpcMethod: "translation.proofread", call: "translation.proofread"},
{name: "translation memory confirm", action: "translation-memory-confirm", body: `{"record_id":"tm-record-1","reviewer":"reviewer","reason":"reviewed"}`, rpcMethod: "translation.memory.confirm", call: "translation.memory.confirm"},
{name: "translation memory resolve conflict", action: "translation-memory-resolve-conflict", body: `{"winner_record_id":"tm-record-1","expected_trusted_record_ids":["tm-record-1","tm-record-2"],"reviewer":"reviewer","reason":"selected"}`, rpcMethod: "translation.memory.resolve_conflict", call: "translation.memory.resolve_conflict"},
{name: "translation glossary add", action: "translation-glossary-add", body: `{"term_id":"term-sensei","source_term":"Sensei","recommended_translation":"老师","review_status":"draft","source":{"source_kind":"manual","observed_unix_seconds":100}}`, rpcMethod: "translation.glossary.add", call: "translation.glossary.add"},
{name: "translation glossary update", action: "translation-glossary-update", body: `{"term_id":"term-sensei","source_term":"Sensei","recommended_translation":"老师","review_status":"draft","reviewer":"reviewer","source":{"source_kind":"manual","observed_unix_seconds":100}}`, rpcMethod: "translation.glossary.update", call: "translation.glossary.update"},
{name: "translation glossary approve", action: "translation-glossary-approve", body: `{"term_id":"term-sensei","reviewer":"reviewer","reason":"approved"}`, rpcMethod: "translation.glossary.approve", call: "translation.glossary.approve"},
@@ -2390,6 +2429,11 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
backend.translationMemoryConfirmParams[0].Reviewer != "reviewer" {
t.Fatalf("TM confirm params=%#v", backend.translationMemoryConfirmParams)
}
if len(backend.translationMemoryResolveParams) != 1 ||
backend.translationMemoryResolveParams[0].WinnerRecordID != "tm-record-1" ||
len(backend.translationMemoryResolveParams[0].ExpectedTrustedRecordIDs) != 2 {
t.Fatalf("TM resolve params=%#v", backend.translationMemoryResolveParams)
}
if len(backend.glossaryMutationParams) != 2 ||
backend.glossaryMutationParams[0].TermID != "term-sensei" ||
backend.glossaryMutationParams[1].Reviewer != "reviewer" ||
@@ -2432,6 +2476,14 @@ func TestAdminControlForwardsAllowlistedActions(t *testing.T) {
t.Fatalf("invalid TM confirm status=%d body=%s", recorder.Code, recorder.Body.String())
}
request = httptest.NewRequest(http.MethodPost, "/admin/control/translation-memory-resolve-conflict", strings.NewReader(`{"winner_record_id":"tm-record-1","expected_trusted_record_ids":["tm-record-1","tm-record-1"],"reviewer":"reviewer","reason":"selected"}`))
request.Header.Set("Authorization", "Bearer control-token")
recorder = httptest.NewRecorder()
s.Handler().ServeHTTP(recorder, request)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("invalid TM resolve status=%d body=%s", recorder.Code, recorder.Body.String())
}
request = httptest.NewRequest(http.MethodPost, "/admin/control/localized-publish", strings.NewReader(`{"from_worker":true,"translation_file":"/tmp/workbench.json"}`))
request.Header.Set("Authorization", "Bearer control-token")
recorder = httptest.NewRecorder()
+38 -1
View File
@@ -512,6 +512,31 @@ paths:
description: Missing or invalid admin token.
"503":
description: Rust bat Translation Memory backend is unavailable.
/admin/translation/memory/conflicts:
get:
summary: List Rust-owned Translation Memory Trusted conflicts
parameters:
- name: limit
in: query
schema:
type: integer
format: int64
minimum: 1
maximum: 1000
default: 100
- name: translation_memory_path
in: query
schema:
type: string
responses:
"200":
description: Translation Memory exact-identity Trusted conflict groups.
"400":
description: Invalid conflict list limit.
"401":
description: Missing or invalid admin token.
"503":
description: Rust bat Translation Memory backend is unavailable.
/admin/translation/glossary/summary:
get:
summary: Read Rust-owned Glossary summary
@@ -637,7 +662,7 @@ paths:
required: true
schema:
type: string
enum: [reload, refresh, restart, sync, verify, repair, catalog-refresh, schedule-add, schedule-update, schedule-remove, schedule-run, task-cancel, translation-task-update, translation-worker-run, translation-proofread, translation-memory-confirm, translation-glossary-add, translation-glossary-update, translation-glossary-approve, translation-glossary-deprecate, translation-glossary-delete, localized-publish, localized-rollback, release-cleanup]
enum: [reload, refresh, restart, sync, verify, repair, catalog-refresh, schedule-add, schedule-update, schedule-remove, schedule-run, task-cancel, translation-task-update, translation-worker-run, translation-proofread, translation-memory-confirm, translation-memory-resolve-conflict, translation-glossary-add, translation-glossary-update, translation-glossary-approve, translation-glossary-deprecate, translation-glossary-delete, localized-publish, localized-rollback, release-cleanup]
requestBody:
required: false
content:
@@ -752,6 +777,18 @@ paths:
type: string
record_id:
type: string
winner_record_id:
type: string
expected_trusted_record_ids:
type: array
items:
type: string
supersede_record_id:
type: string
reviewer:
type: string
reason:
type: string
term_id:
type: string
source_term:
+10
View File
@@ -93,6 +93,8 @@ type TranslationMemoryBackend interface {
TranslationMemorySummary(ctx context.Context, params backendrpc.TranslationMemorySummaryParams) (*backendrpc.TranslationMemorySummaryReport, error)
TranslationMemoryQuery(ctx context.Context, params backendrpc.TranslationMemoryQueryParams) (*backendrpc.TranslationMemoryQueryReport, error)
TranslationMemoryConfirm(ctx context.Context, params backendrpc.TranslationMemoryConfirmParams) (*backendrpc.TranslationMemoryConfirmReport, error)
TranslationMemoryConflicts(ctx context.Context, params backendrpc.TranslationMemoryConflictsParams) (*backendrpc.TranslationMemoryConflictsReport, error)
TranslationMemoryResolveConflict(ctx context.Context, params backendrpc.TranslationMemoryResolveConflictParams) (*backendrpc.TranslationMemoryResolveConflictReport, error)
}
// GlossaryBackend exposes Rust-owned Glossary management and diagnostics.
@@ -238,6 +240,14 @@ func (r RPCClient) TranslationMemoryConfirm(ctx context.Context, params backendr
return r.Client.TranslationMemoryConfirm(ctx, params)
}
func (r RPCClient) TranslationMemoryConflicts(ctx context.Context, params backendrpc.TranslationMemoryConflictsParams) (*backendrpc.TranslationMemoryConflictsReport, error) {
return r.Client.TranslationMemoryConflicts(ctx, params)
}
func (r RPCClient) TranslationMemoryResolveConflict(ctx context.Context, params backendrpc.TranslationMemoryResolveConflictParams) (*backendrpc.TranslationMemoryResolveConflictReport, error) {
return r.Client.TranslationMemoryResolveConflict(ctx, params)
}
func (r RPCClient) GlossarySummary(ctx context.Context, params backendrpc.GlossarySummaryParams) (*backendrpc.GlossarySummaryReport, error) {
return r.Client.GlossarySummary(ctx, params)
}
+2
View File
@@ -80,6 +80,7 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("/admin/translation/handoff", s.handleAdminTranslationHandoff)
mux.HandleFunc("/admin/translation/memory/summary", s.handleAdminTranslationMemorySummary)
mux.HandleFunc("/admin/translation/memory/query", s.handleAdminTranslationMemoryQuery)
mux.HandleFunc("/admin/translation/memory/conflicts", s.handleAdminTranslationMemoryConflicts)
mux.HandleFunc("/admin/translation/glossary/summary", s.handleAdminGlossarySummary)
mux.HandleFunc("/admin/translation/glossary/query", s.handleAdminGlossaryQuery)
mux.HandleFunc("/admin/translation/glossary/diagnose", s.handleAdminGlossaryDiagnose)
@@ -202,6 +203,7 @@ func (s *Server) handleRoot(w http.ResponseWriter, r *http.Request) {
"/admin/translation/handoff",
"/admin/translation/memory/summary",
"/admin/translation/memory/query",
"/admin/translation/memory/conflicts",
"/admin/translation/glossary/summary",
"/admin/translation/glossary/query",
"/admin/translation/glossary/diagnose",
+1 -1
View File
@@ -14,7 +14,7 @@ contract fixture。JSON 由 Rust 代码路径产出后归一化,只替换本
window 为 7260 秒(默认 3600 秒验证周期和 60 秒失败重试周期)。
- 对应 release 的 `official-sync-snapshot.json`
- `launcher_metadata``game_main_config_bootstrap` 的 Go mirror 解码。
- Rust Glossary V2 query 响应,覆盖 alias、approved review、source provenance 和完整 history。
- Rust Glossary domain/feature contract V1、SQLite persistence schema V2 的 query 响应,覆盖 alias、approved review、source provenance 和完整 history。
这些 fixture 只用于 schema / mirror 回归,不代表真实资源版本,也不替代 live
daemon socket 或完整发布切换验证。
+66 -9
View File
@@ -479,9 +479,10 @@ const (
type TranslationMemoryMatchKind string
const (
TranslationMemoryMatchStrongExact TranslationMemoryMatchKind = "strong_exact"
TranslationMemoryMatchCandidateExact TranslationMemoryMatchKind = "candidate_exact"
TranslationMemoryMatchSourceOnly TranslationMemoryMatchKind = "source_only"
TranslationMemoryMatchStrongExact TranslationMemoryMatchKind = "strong_exact"
TranslationMemoryMatchTrustedConflict TranslationMemoryMatchKind = "trusted_conflict"
TranslationMemoryMatchCandidateExact TranslationMemoryMatchKind = "candidate_exact"
TranslationMemoryMatchSourceOnly TranslationMemoryMatchKind = "source_only"
)
// TranslationMemorySummaryParams selects an optional Rust-owned TM database.
@@ -504,16 +505,19 @@ type TranslationMemoryConfirmParams struct {
RecordID string `json:"record_id"`
Reviewer string `json:"reviewer"`
Reason string `json:"reason,omitempty"`
SupersedeRecordID string `json:"supersede_record_id,omitempty"`
}
// TranslationMemorySummary mirrors translation.memory.summary data.
type TranslationMemorySummary struct {
SchemaVersion uint64 `json:"schema_version"`
RecordCount uint64 `json:"record_count"`
TrustedCount uint64 `json:"trusted_count"`
CandidateCount uint64 `json:"candidate_count"`
SupersededCount uint64 `json:"superseded_count"`
RejectedCount uint64 `json:"rejected_count"`
SchemaVersion uint64 `json:"schema_version"`
RecordCount uint64 `json:"record_count"`
TrustedCount uint64 `json:"trusted_count"`
CandidateCount uint64 `json:"candidate_count"`
SupersededCount uint64 `json:"superseded_count"`
RejectedCount uint64 `json:"rejected_count"`
TrustedConflictGroupCount uint64 `json:"trusted_conflict_group_count"`
CurrentTrustedCount uint64 `json:"current_trusted_count,omitempty"`
}
// TranslationMemorySummaryReport distinguishes a missing database from an
@@ -591,6 +595,47 @@ type TranslationMemoryConfirmReport struct {
Entry TranslationMemoryEntry `json:"entry"`
}
// TranslationMemoryConflict groups all records sharing one exact source
// identity when more than one current Trusted record exists.
type TranslationMemoryConflict struct {
SourceText string `json:"source_text"`
SourceHash string `json:"source_hash"`
SourceContext TranslationMemoryContext `json:"source_context,omitempty"`
SourceContextHash string `json:"source_context_hash"`
TrustedRecordIDs []string `json:"trusted_record_ids"`
Records []TranslationMemoryEntry `json:"records"`
}
// TranslationMemoryConflictsParams lists current Trusted conflicts.
type TranslationMemoryConflictsParams struct {
TranslationMemoryPath string `json:"translation_memory_path,omitempty"`
Limit *uint64 `json:"limit,omitempty"`
}
// TranslationMemoryConflictsReport mirrors translation.memory.conflicts data.
type TranslationMemoryConflictsReport struct {
Available bool `json:"available"`
Path string `json:"path"`
Conflicts []TranslationMemoryConflict `json:"conflicts"`
Reason string `json:"reason,omitempty"`
}
// TranslationMemoryResolveConflictParams resolves one exact-identity conflict.
type TranslationMemoryResolveConflictParams struct {
TranslationMemoryPath string `json:"translation_memory_path,omitempty"`
WinnerRecordID string `json:"winner_record_id"`
ExpectedTrustedRecordIDs []string `json:"expected_trusted_record_ids"`
Reviewer string `json:"reviewer"`
Reason string `json:"reason"`
}
// TranslationMemoryResolveConflictReport mirrors the conflict resolution result.
type TranslationMemoryResolveConflictReport struct {
Available bool `json:"available"`
Path string `json:"path"`
Entry TranslationMemoryEntry `json:"entry"`
}
// GlossaryReviewStatus is the Rust-owned term review state.
type GlossaryReviewStatus string
@@ -1067,6 +1112,18 @@ func (c *Client) TranslationMemoryConfirm(ctx context.Context, params Translatio
return &out, err
}
func (c *Client) TranslationMemoryConflicts(ctx context.Context, params TranslationMemoryConflictsParams) (*TranslationMemoryConflictsReport, error) {
var out TranslationMemoryConflictsReport
_, err := c.Call(ctx, "translation.memory.conflicts", params, &out)
return &out, err
}
func (c *Client) TranslationMemoryResolveConflict(ctx context.Context, params TranslationMemoryResolveConflictParams) (*TranslationMemoryResolveConflictReport, error) {
var out TranslationMemoryResolveConflictReport
_, err := c.Call(ctx, "translation.memory.resolve_conflict", params, &out)
return &out, err
}
func (c *Client) GlossarySummary(ctx context.Context, params GlossarySummaryParams) (*GlossarySummaryReport, error) {
var out GlossarySummaryReport
_, err := c.Call(ctx, "translation.glossary.summary", params, &out)
+80 -9
View File
@@ -756,14 +756,15 @@ func TestTranslationMemoryTypedContract(t *testing.T) {
Data: map[string]any{
"available": true,
"path": "/var/lib/bat/translation-memory.sqlite",
"schema_version": 1,
"schema_version": 2,
"summary": map[string]any{
"schema_version": 1,
"record_count": 3,
"trusted_count": 1,
"candidate_count": 1,
"superseded_count": 1,
"rejected_count": 0,
"schema_version": 2,
"record_count": 3,
"trusted_count": 1,
"candidate_count": 1,
"superseded_count": 1,
"rejected_count": 0,
"current_trusted_count": 1,
},
},
},
@@ -859,6 +860,50 @@ func TestTranslationMemoryTypedContract(t *testing.T) {
},
},
}
case "translation.memory.conflicts":
var params TranslationMemoryConflictsParams
if err := json.Unmarshal(req.Params, &params); err != nil {
t.Fatalf("decode conflicts params: %v", err)
}
if params.TranslationMemoryPath != "/var/lib/bat/translation-memory.sqlite" ||
params.Limit == nil || *params.Limit != limit {
t.Fatalf("conflicts params=%#v", params)
}
return testResponse{
Result: testEnvelope{
OK: true, Status: "ok", RequestID: "req-tm-conflicts",
Data: map[string]any{
"available": true,
"path": "/var/lib/bat/translation-memory.sqlite",
"conflicts": []any{},
},
},
}
case "translation.memory.resolve_conflict":
var params TranslationMemoryResolveConflictParams
if err := json.Unmarshal(req.Params, &params); err != nil {
t.Fatalf("decode resolve params: %v", err)
}
if params.WinnerRecordID != "tm-record-1" ||
len(params.ExpectedTrustedRecordIDs) != 2 ||
params.ExpectedTrustedRecordIDs[1] != "tm-record-2" ||
params.Reviewer != "reviewer" ||
params.Reason != "selected" {
t.Fatalf("resolve params=%#v", params)
}
return testResponse{
Result: testEnvelope{
OK: true, Status: "ok", RequestID: "req-tm-resolve",
Data: map[string]any{
"available": true,
"path": "/var/lib/bat/translation-memory.sqlite",
"entry": map[string]any{
"record_id": "tm-record-1",
"trust_status": "trusted",
},
},
},
}
default:
t.Fatalf("unexpected method %q", req.Method)
return testResponse{}
@@ -915,6 +960,32 @@ func TestTranslationMemoryTypedContract(t *testing.T) {
confirmed.Entry.RecordID != "tm-record-1" {
t.Fatalf("confirmed=%#v", confirmed)
}
conflicts, err := client.TranslationMemoryConflicts(context.Background(), TranslationMemoryConflictsParams{
TranslationMemoryPath: "/var/lib/bat/translation-memory.sqlite",
Limit: &limit,
})
if err != nil {
t.Fatalf("TranslationMemoryConflicts error: %v", err)
}
if !conflicts.Available || len(conflicts.Conflicts) != 0 {
t.Fatalf("conflicts=%#v", conflicts)
}
resolved, err := client.TranslationMemoryResolveConflict(context.Background(), TranslationMemoryResolveConflictParams{
TranslationMemoryPath: "/var/lib/bat/translation-memory.sqlite",
WinnerRecordID: "tm-record-1",
ExpectedTrustedRecordIDs: []string{"tm-record-1", "tm-record-2"},
Reviewer: "reviewer",
Reason: "selected",
})
if err != nil {
t.Fatalf("TranslationMemoryResolveConflict error: %v", err)
}
if !resolved.Available || resolved.Entry.RecordID != "tm-record-1" ||
resolved.Entry.TrustStatus != TranslationMemoryStatusTrusted {
t.Fatalf("resolved=%#v", resolved)
}
}
func TestTranslationProofreadUsesRustMethod(t *testing.T) {
@@ -1064,9 +1135,9 @@ func TestGlossaryTypedContract(t *testing.T) {
Data: map[string]any{
"available": true,
"path": "/var/lib/bat/glossary.sqlite",
"schema_version": 1,
"schema_version": 2,
"summary": map[string]any{
"schema_version": 1,
"schema_version": 2,
"term_count": 2,
"approved_count": 1,
"draft_count": 1,