The Trinity Beast – TBI Translation Engine

Custom Bedrock-powered document translation service — 8-pass sentinel system, document-level preprocessor with lxml validation, auto-scaling ECS worker fleet

Region: us-east-2 (Ohio) Version: v6.1 September 6, 2026

1. Why a Custom Translation Engine

The Trinity Beast Infrastructure maintains 44 technical documents, 43 of them translated into 11 languages — 473 translated files in total. The original approach used AWS Translate batch jobs. It worked for simple prose but failed catastrophically on technical documentation.

1.1 Where AWS Translate Fails

AWS Translate is a neural machine translation service optimized for general-purpose text. Technical documentation with embedded code, diagrams, and brand terminology exposes its fundamental limitations:

Failure ModeExampleImpact
Translates code blocksfunction getName() → función obtenerNombre()Code no longer executes
Translates variable namesapi_key → clave_apiDocumentation references break
Breaks Mermaid diagramsTranslates node labels inside mermaid blocksDiagrams fail to render
Corrupts HTML structureMerges adjacent elements, drops attributesStyling and layout break
Transliterates brand namesAutoOps → آٹو آپس (Urdu phonetic)Brand identity lost, search breaks
Localizes numeric units32 GB → 32 Go (French)Technical specs become ambiguous
Drops version numbersPostgreSQL 17.7 → PostgreSQLVersion-specific guidance lost
Ignores translate attributeTranslates content inside protected zonesDefeats the HTML5 standard mechanism

1.2 The Scale Problem

With 40 documents × 11 languages, every documentation update triggers a translation cascade. Before the custom engine:

1.3 The Solution

A custom Bedrock-powered translation engine that understands the boundary between human language and machine language. The engine uses defense-in-depth across the full pipeline:

Result: A single POST /admin/translate call translates any document from any of the 127 catalog languages into up to 12 target languages per request ("all" means our library's internal 11), each code resolved through the language catalog and its alternate codes. It deploys to S3, invalidates CloudFront, rebuilds the search index, and emails a summary. The source language is the one stated or declared for the document, then a definitive detection, otherwise auto — no pivot through English required.

2. Architecture

2.1 Pipeline Flow

The translation service is an event-driven pipeline that decouples submission from execution. The operator submits a job; the system handles everything else asynchronously.

Diagram 2.1: End-to-End Pipeline Architecture (v6.0 — BeastTranslate)

flowchart TB
    subgraph IN["Entry Points"]
        A["POST /admin/translate"]
        A2["Customer accept: staged sources, quote job"]
    end
    subgraph LPO["LPO Server (Go)"]
        B["Validate and enqueue: 127-language catalog, up to 12 languages, 42 docs"]
        C["Valkey: live state"]
        D["Aurora: job row inserted before any send"]
    end
    subgraph AQ["AWS Queue"]
        E["SQS Queue: one message per language"]
    end
    subgraph BTS["BeastTranslate — Persistent ECS Service"]
        direction TB
        BT["SQS Long-Poll Loop"]
        BT --> SC{"Scale Check"}
        SC -->|Single lang| BT2["Process In-Place"]
        SC -->|Multi-lang| BT3["Scale Service to N, at most 12"]
        BT3 --> BT4["N Containers Poll Same Queue"]
        BT4 --> BT5["Each Takes 1 Language Message"]
        BT2 --> H00
        BT5 --> H00
        subgraph TI["Translation Intelligence (Python)"]
            direction LR
            H00["Language Runtime: catalog row + profile, 15-min cache, snapshot fallback"]
            H0["Source Validation"]
            H1["Complexity Analysis"]
            H2["Document Preprocessor"]
            H3["Sentinel System — 4 Types"]
            H4["Bedrock — 3-Region Failover"]
            H5["Validator — Hard + Soft Tiers"]
            H6["Integrity Check + Auto-Repair"]
            H00 --> H0 --> H1 --> H2 --> H3 --> H4 --> H5 --> H6
        end
    end
    subgraph DEL["Delivery"]
        direction LR
        I["Worker writes S3"]
        J["CloudFront Invalidation (library docs)"]
        K["POST /admin/translate/complete: state, search rebuild, email, scale-down"]
    end
    A2 -->|calls| A
    A --> B
    B --> C
    B --> D
    B --> E
    E --> BT
    H6 --> I
    I --> J
    I --> K
    style IN fill:#1e293b,stroke:#60a5fa,color:#e2e8f0
    style LPO fill:#1e293b,stroke:#94a3b8,color:#e2e8f0
    style AQ fill:#1e293b,stroke:#a78bfa,color:#e2e8f0
    style BTS fill:#1e293b,stroke:#fb923c,color:#e2e8f0
    style TI fill:#253347,stroke:#fbbf24,color:#e2e8f0
    style DEL fill:#1e293b,stroke:#10b981,color:#e2e8f0
    style A fill:#1e3a5f,stroke:#60a5fa,color:#e2e8f0
    style A2 fill:#1e3a5f,stroke:#60a5fa,color:#e2e8f0
    style B fill:#334155,stroke:#94a3b8,color:#e2e8f0
    style C fill:#064e3b,stroke:#10b981,color:#e2e8f0
    style D fill:#064e3b,stroke:#10b981,color:#e2e8f0
    style E fill:#2e1065,stroke:#a78bfa,color:#e2e8f0
    style BT fill:#7c2d12,stroke:#fb923c,color:#e2e8f0
    style SC fill:#7c2d12,stroke:#fb923c,color:#e2e8f0
    style BT2 fill:#7c2d12,stroke:#fb923c,color:#e2e8f0
    style BT3 fill:#7c2d12,stroke:#fb923c,color:#e2e8f0
    style BT4 fill:#7c2d12,stroke:#fb923c,color:#e2e8f0
    style BT5 fill:#7c2d12,stroke:#fb923c,color:#e2e8f0
    style H00 fill:#1e3a5f,stroke:#60a5fa,color:#e2e8f0
    style H0 fill:#92400e,stroke:#fbbf24,color:#e2e8f0
    style H1 fill:#92400e,stroke:#fbbf24,color:#e2e8f0
    style H2 fill:#92400e,stroke:#fbbf24,color:#e2e8f0
    style H3 fill:#92400e,stroke:#fbbf24,color:#e2e8f0
    style H4 fill:#92400e,stroke:#fbbf24,color:#e2e8f0
    style H5 fill:#92400e,stroke:#fbbf24,color:#e2e8f0
    style H6 fill:#92400e,stroke:#fbbf24,color:#e2e8f0
    style I fill:#064e3b,stroke:#10b981,color:#e2e8f0
    style J fill:#064e3b,stroke:#10b981,color:#e2e8f0
    style K fill:#064e3b,stroke:#10b981,color:#e2e8f0
    linkStyle 0,1,2,3,4,5,6 stroke:#fb923c,stroke-width:2px
    linkStyle 7,8,9,10,11,12,13 stroke:#fbbf24,stroke-width:2px
    linkStyle 14,15 stroke:#60a5fa,stroke-width:2px
    linkStyle 16,17 stroke:#10b981,stroke-width:1.5px
    linkStyle 18,19 stroke:#a78bfa,stroke-width:2.5px
    linkStyle 20,21,22 stroke:#10b981,stroke-width:2px
        

2.2 Components

ComponentTypeRuntimePurpose
POST /admin/translate (+ 30 more, including refund-sweep)Admin APIGoJob submission, monitoring, control
trinity-beast-translation-queueSQS—One message per language — workers poll directly
tbi-translate-workerECS Fargate ServicePython 3.11Bedrock translation + sentinel + validation + S3 write + CF invalidation (no timeout ceiling)
/admin/translate/completeLPO EndpointGoSearch rebuild + SES notification + state transition + cost computation + scale-down
translation_jobsAurora table—Permanent job records (28 columns)
translation_job_eventsAurora table—Granular per-doc/lang audit log

2.3 Why Python (The Only Python in the Fleet)

Every other compute workload in The Trinity Beast Infrastructure is written in Go. The translation worker is the sole exception, and for good reason:

Convention note: All Lambda functions use 1770 MB memory (multiple of 3). The worker runs as an ECS Fargate task (2 vCPU / 6 GB) with no timeout ceiling — large documents translate to completion regardless of processing time. Deploy and finalize Lambdas use 60s and 180s timeouts respectively.

3. Sentinel Preprocessing System

The sentinel system is the core innovation that makes reliable technical document translation possible. It operates on a simple principle: the model cannot corrupt what it never sees.

Before any chunk is sent to Bedrock, protected content is replaced with placeholder tokens. The model translates the prose around the placeholders. After translation, the placeholders are swapped back to the original content. Validation then confirms everything survived intact.

3.0 Pre-Sentinelization Passes (v5.0)

Before the sentinel system runs, three automatic preparation passes ensure maximum coverage. These run deterministically on every document — no manual markup needed. The design principle: they can't break what isn't there.

PassFunctionWhat It DoesFailure Class Eliminated
Brand Term Auto-Wrap_auto_wrap_brand_terms()Scans for all 60 protected terms from translation-config.json and wraps each occurrence in <span translate="no">. They become Type A sentinel candidates automatically.Brand transliteration (e.g., The Trinity Beast → ट्रिनिटी बीस्ट)
Code Tag Protection_protect_code_tags()Upgrades every bare <code> tag to <code translate="no">. Pass 1 then lifts them all as Type A sentinels.Code tag mismatch failures (the single largest failure class before v5.0)
Path Protection_fix_paths_absolute()Converts relative paths (assets/, images/, css/, js/) to absolute URLs before translation. The model passes them through unchanged.Broken asset references in translated docs

These passes run in handler.py before the document enters the chunker or sentinel system. They are deterministic, idempotent, and add zero cost — they only manipulate the source HTML locally. After these passes, the sentinel system has maximum coverage: every code tag, every brand term, and every path is already protected before sentinelization begins.

3.1 Sentinel Types and Eight Passes

Type A — Full Element Extraction (__TBP{N}__)

Replaces entire translate="no" elements with a single token. The model sees only the placeholder and places it in the natural position for the target language's word order.

BeforeAfter Sentinel Pass
<span translate="no">CloudFront</span> invalidation __TBP0__ invalidation
<code translate="no">api_key</code> parameter __TBP1__ parameter

Handles arbitrary nesting depth — processes innermost elements first, then sweeps outward until stable.

Type B — Paired Open/Close (__TBO{N}__ / __TBC{N}__)

For plain <span> wrappers (no class attribute) containing translatable text. The wrapper tags become sentinels; the text between them is translated normally. Spans with a class attribute are now extracted as Type A FULL sentinels (see Pass 1b below) since the class denotes a structural/decorative CSS hook that must survive intact.

BeforeAfter Sentinel Pass
<span style="color:#9ece6a">success message</span> __TBO0__success message__TBC0__

The model translates "success message" while the <span style="..."> wrapper survives intact. Class-bearing spans like <span class="badge">, <span class="tree-label">, and <span class="method-tag"> are handled by Pass 1b as full extractions — the model never sees them.

Type C — Numeric Protection (__TBN{N}__)

Protects bare numbers in prose from the model's tendency to drop, paraphrase, or localize them. Matches integers, decimals, percentages, and number+unit pairs.

BeforeAfter Sentinel PassProblem Prevented
uses 1770 MB of memory uses __TBN0__ of memory French translating "MB" → "Mo"
achieves 98.5% uptime achieves __TBN1__ uptime Japanese dropping the decimal
62% cache hit rate __TBN2__ cache hit rate German paraphrasing to words

Type D — Brand Term Protection (__TBT{N}__)

Protects brand terms, product names, and proper nouns that must never be translated or transliterated. Unlike Type A (which requires translate="no" in the source HTML), Type D operates from a centralized configuration list — no source markup needed.

BeforeAfter Sentinel PassProblem Prevented
powered by The Trinity Beast powered by __TBT0__ Hindi transliterating to ट्रिनिटी बीस्ट
deployed on CloudFront deployed on __TBT1__ Arabic transliterating to كلاود فرونت
Cory Dean Kalani __TBT2__ Urdu transliterating person names

Protected terms are defined in translation-config.json (57 terms). The sentinel pass matches terms using word-boundary regex for short terms (≤5 chars) and substring matching for longer terms. Restoration is exact — the original term text is re-injected at the sentinel position.

Sentinel Recovery Pass (Post-Restoration)

Complex-script models (Hindi, Urdu, Arabic) occasionally drop sentinel tokens entirely from their output — the token simply doesn't appear in the translated text. This affects both Type A (FULL) and Type D (TERM) sentinels, particularly in token-dense chunks with 20+ sentinels. The recovery pass runs after normal restoration and before validation:

  1. Iterates all TERM entries in the sentinels list
  2. Checks if the term is present in the source but missing from the restored output
  3. Re-injects the original term text at an approximate position (ratio-based paragraph matching)
  4. Falls back to insertion before the last closing tag if position cannot be determined

This eliminates the class of failures where the model acknowledges the sentinel in its "thinking" but omits it from the output — a behavior observed primarily in Indic scripts with token-dense chunks.

3.2 Processing Flow

Diagram 3.1: Sentinel Preprocessing Flow

flowchart TD
    A[Source HTML Chunk] --> B[Pass 1: Extract translate=no elements]
    B --> B1b[Pass 1b: Extract class-bearing spans as FULL sentinels]
    B1b --> B2[Pass 2: Extract all text-only spans as FULL sentinels]
    B2 --> B3[Pass 2b: Wrap text-only links as paired sentinels]
    B3 --> B4[Pass 2c: Extract nested-HTML and event-handler links as FULL]
    B4 --> D[Pass 3: Replace bare numbers with numeric sentinels]
    D --> D2[Pass 4: Replace brand terms with TERM sentinels]
    D2 --> D3[Pass 5: Extract email addresses as FULL sentinels]
    D3 --> D4[Pass 6: Extract bare prose URLs as FULL sentinels]
    D4 --> E[Send to Bedrock with sentinel-aware prompt]
    E --> F[Receive translated chunk with sentinels intact]
    F --> G[Deduplicate any model-doubled paired sentinels]
    G --> H[Restore sentinels high-to-low index order]
    H --> H2[Recovery pass: re-inject any dropped FULL/TERM sentinels]
    H2 --> H3[Repair pass: fix dropped span/strong/em tags]
    H3 --> I[Run validators against source + restored output]
    I -->|PASS| J[Accept chunk]
    I -->|FAIL| K{Retries remaining?}
    K -->|Yes| L[Retry with strict prompt + temperature jitter]
    L --> E
    K -->|No| M[Raise TranslationError]

    style A fill:#1e3a5f,stroke:#60a5fa,color:#e2e8f0
    style E fill:#92400e,stroke:#fbbf24,color:#e2e8f0
    style J fill:#064e3b,stroke:#10b981,color:#e2e8f0
    style M fill:#450a0a,stroke:#ef4444,color:#e2e8f0
        

The eight passes execute in strict order — later passes operate on the output of earlier ones. Pass 1b extracts all class-bearing spans as FULL sentinels (eliminating the entire class of span-drop failures for structural/decorative CSS hooks). Pass 2 extracts remaining text-only spans as FULL sentinels. Passes 2b/2c handle anchor tags — text-only links become PAIR sentinels (attributes opaque, inner text translated), while links with nested HTML or JavaScript event handlers are extracted as FULL. Type C (numeric) sentinels protect numbers that appear inside Type B (paired) text. Type D (brand term) sentinels protect terms anywhere in translatable content. Passes 5 and 6 protect email addresses and bare prose URLs from translation/mangling. This provides defense-in-depth across eight layers.

3.3 Restoration and Deduplication

After translation, sentinels are restored in reverse index order (high → low) to prevent prefix collisions (__TBP1__ must not match inside __TBP10__).

A deduplication pass runs before restoration to handle a known model behavior: occasionally the model emits a paired sentinel twice consecutively (a bilingual output instinct). The deduplicator collapses __TBO0__text__TBC0__ __TBO0__text__TBC0__ into a single occurrence.

4. Validator System

Every translated chunk is validated against the source before acceptance. Validators enforce structural integrity and content preservation — if a translation passes all validators, it is guaranteed to be functionally correct (code works, links resolve, diagrams render).

4.1 Validation Checks

ValidatorTypeWhat It ChecksFailure Example
check_protected_termsHardEvery protected term in source appears in output"CloudFront" missing from Japanese output
check_version_numbersHardAll version numbers (X.Y.Z) survive translation"17.7" dropped from PostgreSQL reference
check_preserve_patternsHardURLs, emails, IPs, ARNs, resource IDs, cron expressions, memory sizesARN truncated or IP address reformatted
check_tag_countsHardHTML tag counts match for structural tagsExtra <span> added or <code> dropped
check_translate_no_zonesHardContent inside translate="no" zones unchangedProtected code block content altered

Protected term matching: Short uppercase acronyms (≤4 chars like SQS, ECR, S3) use word-boundary matching to avoid false positives where the acronym appears as a substring (e.g., "ECR" inside "SECRET"). Longer terms use plain substring matching.

Implementation (v2.5): The check_tag_counts and check_translate_no_zones validators use character scanning with exact boundary matching — no regex. We control these tags. We know that a tag starts with < and ends with >. The scanner finds complete opening tags by looking for <tagname followed by a boundary character (>, space, tab, newline, or /), then reads to the closing >. This eliminates false positives from partial regex matches and is immune to edge cases where tag names appear as text content (e.g., documenting translate="no" as literal text inside a code tag).

4.2 Retry Strategy

When validation fails, the engine retries with two progressive adjustments:

  1. Strict prompt activation — adds an explicit warning: "PREVIOUS ATTEMPT FAILED VALIDATION. Be more careful: every protected term and every version number from the input MUST appear unchanged in the output."
  2. Temperature jitter — increments temperature by 0.1 per retry (0.0 → 0.1 → 0.2 → 0.3, capped at 0.5). A deterministic temp=0 retry produces the same erroneous output; temperature jitter lets the model take a different sampling path.

Underneath every attempt, each Bedrock call has its own size and its own clock (October 2026). A call asks for an output cap sized from its input: the estimated input tokens × the target language's measured output ratio × 1.5, never below 4,096 and never above 49,152 tokens. Its read timeout is sized from that cap: 60 seconds plus the cap divided by the model's slowest measured speed (35 tokens per second for Qwen, 25 for Claude), rounded up to the minute and never above 900 seconds. A small chunk therefore waits minutes, not a quarter of an hour, before a stalled call is abandoned.

If a reply stops exactly at its cap, the call is repeated once at the full 49,152-token ceiling (CAP-ESCALATED in the log), so a legitimately long translation is never cut short. That escalation happens at most once per chunk: every later attempt of the same chunk starts at the raised cap and is never raised again. The SDK's own retries are switched off, so the validation loop above and the regional failover in §12 are the only retry layers.

Maximum retries: 3, set per language in translation_language_profiles.max_retries and read at job start, with the value in translation-config.json as the fallback when Aurora is unreachable. If all attempts fail, a TranslationError is raised with the chunk index, validator detail, and a preview of the problematic chunk.

The retry count is per language, and that matters more than it looks. Three retries is what makes the temperature progression above reachable in full — 0.0 on the first attempt, then 0.1, 0.2 and 0.3. A language configured with max_retries = 1 only ever sees 0.0 and 0.1, so it gets one jittered attempt rather than three, and a failure it would have escaped on the third pass is recorded as a hard failure instead.

This was live for real. On 2026-08-30 the profiles held 3 for ar, hi, ja, ur and zh, 2 for ru, and 1 for de, es, fr, it and pt. The three languages that had been failing repeatedly on one document were es, pt and it — three of the five with the fewest attempts. A failure called "deterministic" on the strength of two identical runs had in fact only ever been given two sampling paths, not four. All eleven languages were set to 3 the same day.

Lesson worth carrying: before concluding a translation failure is deterministic, check how many attempts that language is actually configured for. A knob with a per-language default is a knob that can silently differ from the one you tested.

4.3 Hard vs Soft Failures

Validators are classified into two tiers based on what they protect:

TierTagsBehaviorRationale
Hard (content-critical)<code>, <pre>, <a>Retry → reject on failureMissing code blocks, broken links, or lost pre-formatted content means the translation is functionally broken
Soft (decorative/structural)<span>, <strong>, <em>, <br>Log warning, pass throughMissing styling wrappers don't break functionality — the post-translation integrity check repairs them

This tiered approach eliminates the failure mode where a correctly-translated document is rejected because the model dropped a single decorative <span> wrapper during RTL reordering. The content is correct — only the styling wrapper is missing — and the integrity check restores it automatically.

The ValidationReport aggregates all results and exposes:

4.4 Post-Translation Integrity Check

After translation completes and chunks are reassembled, a full-document integrity check runs before the S3 write. This is the defense-in-depth layer — it repairs structural drift that the per-chunk validator intentionally allows through (soft failures).

Repair Capabilities

IssueDetectionRepair Action
</br> injectionString scan for invalid closing br tagsStrip all occurrences (never valid HTML)
<br> inside Mermaid blocksRegex scan within <pre class="mermaid">Remove (breaks Mermaid syntax)
Mermaid content corruptionByte-for-byte comparison with sourceFlag as warning (cannot auto-repair content changes)
Missing translate="no" span wrappersCompare source protected elements to outputRe-wrap bare content with original element tags
Missing <strong>/<em> wrappersSame pattern as span recoveryRe-wrap bare content

The integrity check only repairs translate="no" elements (where content is byte-for-byte identical between source and output). For translated content that lost its wrapper, the check logs the discrepancy but cannot reliably re-wrap (the content has been translated — matching it to the source wrapper requires semantic understanding).

Design principle: If the translated content is present and correct but the HTML structure is degraded, repair it. Only flag as unrecoverable if content is actually missing or corrupted. The customer sees a clean translation — the repairs happen invisibly.

4.5 Source Document Validation (v2.8)

Before any translation work begins, the source document passes through a validation gate. This catches defects that would cause translation failures or produce broken output — rejecting early saves Bedrock tokens and prevents corrupted translations from reaching S3.

Defect Categories

CategoryWhat It CatchesAuto-Repairable?
STRUCTURALUnclosed tags, malformed HTML, nesting violationsYes (up to 5 unclosed tags)
MERMAIDEmpty diagram blocks, missing type declaration, mismatched bracketsNo — reject with location
ENCODINGBOM markers, null bytes, mixed encodingsYes (strip BOM/nulls)
SIZEDocument exceeds 500,000 characters (counted on decoded text, not bytes), excessive nesting depth (>30 levels)No — reject with size info
CONFLICTtranslate="no" on root element (nothing to translate)No — reject immediately

Validation Flow

  1. Size check — reject if > 500,000 characters (chunking becomes unreliable at this size). This is the worker's backstop and it rejects only internal jobs: customer quote jobs run with skip_validation: true, so this check never rejects them, though its repairs still apply. Customer documents are already bounded by the separate 300 KB (307,200-byte) cap, enforced by the LPO at the quote and again at the accept re-fetch. The two limits measure different things and are never the same number.
  2. Encoding check — detect and strip BOM markers, null bytes; flag mixed encodings
  3. Structural HTML check — scan for unclosed tags; auto-repair up to 5 by appending closing tags at the correct nesting level
  4. Mermaid syntax check — validate every <pre class="mermaid"> block has a valid diagram type, balanced brackets, and non-empty content
  5. Conflict check — reject if the root <body> or <html> element has translate="no"

Rejection vs Repair

The validator follows a strict philosophy: try to fix it silently, reject early if you can't. Repairable issues (unclosed tags, BOM markers) are fixed in-place — the customer never knows. Unrecoverable issues produce an actionable defect report with the exact location, what's wrong, and how to fix it. For customer quote jobs (skip_validation: true) the repairs run and the rejections do not, because those documents were already checked at the quote and at accept; internal jobs get both.

ValidationResult:
  valid: false
  rejection_reason: "2 unrecoverable defects found"
  defects:
    - severity: error
      category: MERMAID
      location: "Section 5, line 342"
      description: "Empty Mermaid block — no diagram content"
      suggestion: "Add diagram content or remove the empty <pre class='mermaid'> block"
    - severity: error
      category: SIZE
      location: "Document root"
      description: "Document is 626,688 characters (612 KB). Maximum is 500,000 characters (488 KB)."
      suggestion: "Split into multiple documents or remove large embedded assets"

Cost savings: A rejected document costs zero Bedrock tokens. Without source validation, a broken document would fail during translation (after burning tokens on partial chunks), produce a corrupted output, and require manual investigation. Source validation catches these cases in <10ms with zero API calls.

4.6 Diagram Integrity (v2.8)

Mermaid diagrams are code — they must survive translation byte-for-byte. The integrity check (section 4.4) now includes dedicated diagram verification with automatic recovery.

Detection

The integrity check counts Mermaid blocks in the source (<pre class="mermaid">) and compares against the translated output. If any diagrams are missing from the output, the auto-stitch mechanism activates.

Auto-Stitch Recovery

When a diagram is missing from the translated output:

  1. Identify which source diagram is absent (by content matching)
  2. Extract the full <div class="diagram-wrap"> block from source (includes label + pre)
  3. Locate the correct insertion point in the output (same section, same relative position)
  4. Inject the source diagram block verbatim — diagrams don't need translation

The stitched diagram is the English version, which is functionally correct — Mermaid syntax is language-independent. The surrounding prose is already translated, so the reader gets translated explanations with a working diagram.

Tag Inventory Integration

The _count_tags function now reports diagram count alongside other structural tags:

Tag Inventory (source → output):
• Trinity-Beast-Performance-Report.html
  IN:  code:75 pre:8 strong:12 em:3 a:6 br:20 diagrams:4
  OUT: code:75 pre:8 strong:12 em:3 a:6 br:20 diagrams:4

If a diagram is lost during translation and auto-stitched back, the final count still matches — the stitch happens before the tag inventory is calculated. A mismatch in the diagrams count after stitching indicates a structural issue that needs manual review.

Result: The Performance Report (75 KB, 4 Mermaid diagrams, 18 sections) translates to French with all 4 diagrams intact — 3 survived translation naturally, 1 was auto-stitched from source. The reader sees no difference.

4.7 Post-Translation Repair Pipeline (v5.0)

Defense-in-depth: even with perfect preprocessing, models occasionally drop or mangle structural elements. The repair pipeline runs after translation and before final validation, catching anything that slipped through.

Code Tag Repair (_repair_code_tags)

Multi-pass repair (up to 6 iterations) that detects dropped <code> wrappers by comparing the translated output against the source document. For each code-tagged value in the source, it searches the output for the bare content and re-wraps it with the original tag. Converges when no more repairs are possible.

Input (broken)Output (repaired)
api_key parameter requires...<code translate="no">api_key</code> parameter requires...
the timestamptz column stores...the <code translate="no">timestamptz</code> column stores...

The repair function matches content from the source's code tags against the output using exact string matching. It handles both self-closing patterns and open/close pairs. Each pass may reveal new repair opportunities (nested cases), hence the multi-pass design with a convergence check.

lxml Syntax Validation (_tidy_validate_and_repair)

Structural HTML repair using lxml's robust parser. Catches and fixes issues that the model introduces in the HTML structure itself — unclosed tags, nesting violations, mismatched attributes, and malformed output. This is the final safety net before the translated chunk is accepted.

Issue DetectedAction
Unclosed <div> or <span>Auto-close at the correct nesting level
Orphaned closing tagsRemove (no matching opener)
Invalid nesting (e.g., <p> inside <p>)Restructure to valid hierarchy
Malformed attributesNormalize quotes and spacing

Uses lxml.html.fragment_fromstring() for parsing — no external binary needed (lxml is in the container's requirements.txt). Falls back to BeautifulSoup if lxml encounters a parse failure it cannot recover from.

Output Integrity Check (_check_output_integrity)

Final gate before a translated chunk is accepted. Compares structural tag counts between source and output — every <code>, <pre>, <strong>, <em>, <a>, <br>, and Mermaid diagram must have matching counts. If counts diverge beyond tolerance, the _recover_missing_wrappers() function attempts targeted restoration before rejecting the chunk.

Per-Language Tag Recovery (tag_recovery/ module — v5.1)

Different languages exhibit different tag-drop patterns. Arabic and Urdu (RTL) tend to drop trailing <br> tags because their paragraph flow doesn't rely on line breaks the same way English does. Hindi sometimes splits or merges <strong> spans when the translated phrase length differs from English. CJK languages occasionally omit <br> at list boundaries. Latin/Cyrillic languages rarely have issues.

Rather than applying a one-size-fits-all fixer, the tag recovery system dispatches to per-language Python scripts — one file per supported language, each encoding the known behavioral patterns for that script type:

ModuleLanguageKnown Patterns
tag_recovery/ar.pyArabicRTL <br> drops, <strong> reordering around punctuation
tag_recovery/ur.pyUrduRTL <br> drops at list boundaries, <strong> merging
tag_recovery/hi.pyHindi<strong> splitting, <br> drops at danda boundaries
tag_recovery/ja.pyJapaneseOccasional <br> drop (continuous flow with ideographic period)
tag_recovery/zh.pyChineseOccasional <br> drop at list boundaries
tag_recovery/es.pySpanishRare — <strong> split on longer phrases
tag_recovery/pt.pyPortugueseRare — similar to Spanish
tag_recovery/fr.pyFrenchRare — restructuring around non-breaking space punctuation
tag_recovery/de.pyGermanRare — compound word <strong> merges
tag_recovery/ru.pyRussianRare — <br> drop in technical passages
tag_recovery/it.pyItalianRare — similar to Spanish/Portuguese

Flow: After Bedrock returns a translated chunk, the dispatcher (tag_recovery/__init__.py) counts tags in both source and output using common.py utilities. If counts match, it skips. If they differ, it loads the language-specific fixer and applies deterministic repairs:

  1. Count — count_tags() counts <br>, <strong>, <em>, <code>, <a>, <span>, <pre>
  2. Diff — Negative = missing, Positive = extra, Zero = match
  3. Fix — Language-specific: inject missing <br> at sentence boundaries, wrap unwrapped <strong> terms, remove spurious extras
  4. Re-count — Confirm fix resolved the diff

All operations use direct string comparison — no regex. The convention: tags are structural, deterministic, and countable. Either the bytes are there or they aren't. Each language file is small, focused, and independently extensible as new patterns emerge.

Design principle: Preprocessing is the fortress — strip everything non-prose before the model sees it. Post-processing is defense-in-depth — repair anything that slips through. The goal is zero repairs needed because prep was thorough, but the safety net is always active. The per-language tag recovery system adds surgical precision — each language gets the fixer it needs, not a generic approximation.

4.8 Prose-Mode Fallback (v5.2)

When the normal translation path (preprocessor + sentinel + Bedrock) produces catastrophic code tag loss, the system automatically re-translates the affected part through prose mode — a fundamentally different translation strategy that preserves structural elements by design.

Trigger Condition

The hint validation system counts <code> tags in each translated part immediately after Bedrock returns. If the drift exceeds tolerance, the fallback fires:

actual_code = count(<code> tags in translated output)
drift = actual_code - hint_code
tolerance = max(3, hint_code × 0.15)

if abs(drift) > tolerance:
    → FALLBACK: re-translate this part via prose mode

Fallback Flow

  1. Detect — Hint validation catches drift beyond tolerance (e.g., expected 15 code tags, got 0)
  2. Extract — Raw-extract all <pre translate="no"> blocks from the source part (byte-perfect preservation)
  3. Fragment — extract_prose() walks the DOM and extracts only translatable text fragments, leaving all HTML structure untouched
  4. Translate — Fragments are sent to Bedrock as a numbered list (plain text, no HTML) — the model cannot damage structure it never sees
  5. Rebuild — rebuild_with_translations() inserts translated fragments back into the original DOM skeleton
  6. Restore — Pre-extracted blocks are placed back at their original positions
  7. Validate — Re-count code tags in the fallback output; accept if within tolerance, keep original if fallback also fails

Why Prose Mode Preserves Code Tags

The normal path sends full HTML to Bedrock — the model must understand and preserve all structural elements while translating. Prose mode inverts this: it never shows the model any HTML. The DOM is the skeleton; only the text content moves through the model. Code tags, pre blocks, spans, and all structural HTML remain in the skeleton untouched — they were never at risk.

Example: Partner-Onboarding × French

Part 3 of this document has density=high, code=15. On the normal path, Qwen consistently dropped all 15 code tags (drift -15). The fallback re-translates through prose mode — 15/15 code tags preserved, validation passes, job succeeds.

Design principle: The normal path is fast (one Bedrock call per chunk). Prose mode is more reliable but slower (one call per fragment batch). The fallback uses prose mode only when the normal path fails validation — giving you speed in the common case and reliability in the edge case. If both paths fail, the original output is kept and the full-document integrity check handles the final disposition.

5. BeastTranslate — Persistent Worker Architecture

Translation execution is handled by BeastTranslate — a persistent ECS Fargate service (tbi-translate-worker-service) that continuously polls the SQS translation queue for jobs. Unlike the previous Step Function → RunTask model (v3.0), BeastTranslate is always-on — no cold starts, no orchestration overhead, instant job pickup.

5.1 BeastTranslate Service Design (v3.1)

Diagram 5.1: BeastTranslate Persistent Worker Architecture

flowchart TD
    SQS["SQS Translation Queue: one message per language"] --> LP["Long-Poll Loop - 20s wait"]
    LP -->|Message received| DM["Deserialize Job Message"]
    DM --> EM{"Multi-Language?"}
    EM -->|Single lang| RT["Process In-Place"]
    EM -->|Multi-lang| BT["Scale Service to N Containers, at most 12"]
    BT --> PAR["N Containers Poll Same Queue"]
    PAR --> EACH["Each Takes 1 Language Message"]
    EACH --> LRT["Load Language Runtime: catalog row + profile, 15-min cache"]
    RT --> LRT
    LRT --> PROC["Translate All Docs for That Language"]
    PROC --> EV["Per-pair events: POST /admin/translate/event/job_id"]
    EV --> CC{"Before publish: job cancelled?"}
    CC -->|No| PUB["Write to S3"]
    CC -->|Yes| CXL["Stop: lang_cancelled, nothing published"]
    CC -->|Customer job, state unknown| UNK["Fail closed: lang_failed cancel-state-unknown"]
    PUB --> LAST{"Last worker of the job?"}
    CXL --> LAST
    UNK --> LAST
    LAST -->|Yes| CMP["POST /admin/translate/complete/job_id"]
    LAST -->|No| DONE
    CMP --> DONE{"More Messages?"}
    DONE -->|Yes| LP
    DONE -->|No - queue empty| IDLE["IDLE - Resume Polling"]
    IDLE --> LP

    subgraph GS["Graceful Shutdown"]
        SIG["SIGTERM Received"] --> FIN["Finish Current Doc"]
        FIN --> CLEANEX["Clean Exit"]
    end

    subgraph VH["Visibility Heartbeat"]
        HB["Every 5 min"] --> EXTVIS["Extend SQS Visibility"]
        EXTVIS --> PREV["Prevent Re-Delivery"]
    end

    style GS fill:#1e293b,stroke:#c084fc,color:#e2e8f0
    style VH fill:#1e293b,stroke:#60a5fa,color:#e2e8f0
    style SQS fill:#2e1065,stroke:#a78bfa,color:#e2e8f0
    style LP fill:#7c2d12,stroke:#fb923c,color:#e2e8f0
    style DM fill:#7c2d12,stroke:#fb923c,color:#e2e8f0
    style EM fill:#1e3a5f,stroke:#60a5fa,color:#e2e8f0
    style RT fill:#92400e,stroke:#fbbf24,color:#e2e8f0
    style BT fill:#92400e,stroke:#fbbf24,color:#e2e8f0
    style PAR fill:#92400e,stroke:#fbbf24,color:#e2e8f0
    style EACH fill:#92400e,stroke:#fbbf24,color:#e2e8f0
    style LRT fill:#1e3a5f,stroke:#60a5fa,color:#e2e8f0
    style PROC fill:#92400e,stroke:#fbbf24,color:#e2e8f0
    style EV fill:#064e3b,stroke:#10b981,color:#e2e8f0
    style CC fill:#7f1d1d,stroke:#f87171,color:#e2e8f0
    style PUB fill:#064e3b,stroke:#10b981,color:#e2e8f0
    style CXL fill:#3f3f46,stroke:#a1a1aa,color:#e2e8f0
    style UNK fill:#7f1d1d,stroke:#f87171,color:#e2e8f0
    style LAST fill:#1e293b,stroke:#334155,color:#e2e8f0
    style CMP fill:#064e3b,stroke:#10b981,color:#e2e8f0
    style DONE fill:#1e293b,stroke:#334155,color:#e2e8f0
    style IDLE fill:#1e293b,stroke:#334155,color:#e2e8f0
    style SIG fill:#4c1d95,stroke:#c084fc,color:#e2e8f0
    style FIN fill:#4c1d95,stroke:#c084fc,color:#e2e8f0
    style CLEANEX fill:#4c1d95,stroke:#c084fc,color:#e2e8f0
    style HB fill:#1e3a5f,stroke:#60a5fa,color:#e2e8f0
    style EXTVIS fill:#1e3a5f,stroke:#60a5fa,color:#e2e8f0
    style PREV fill:#1e3a5f,stroke:#60a5fa,color:#e2e8f0
    linkStyle 0,1 stroke:#a78bfa,stroke-width:2px
    linkStyle 11,12,15,18,20 stroke:#10b981,stroke-width:2px
    linkStyle 14,17 stroke:#f87171,stroke-width:2px
        

Service Specification

PropertyValue
Service Nametbi-translate-worker-service
Cluster NodeBeastTranslate
Image211998422884.dkr.ecr.us-east-2.amazonaws.com/tbi-translate-worker:latest
Resources2 vCPU / 6 GB (per container)
Default Desired Count1
Max Scale12 (one per language of a job; a quote holds at most 12)
RuntimePython 3.11
Entry Pointtask_runner.py
IAM Roletbi-translate-role
Queuetrinity-beast-translation-queue
Log Group/ecs/tbi-translate-worker
TimeoutNone — runs to completion

How It Works: The Polling Loop

BeastTranslate runs a continuous polling loop. When no jobs exist, it idles — consuming negligible CPU while maintaining a warm connection to SQS. The moment a translation job is submitted, the container picks it up within seconds (no cold start, no orchestration delay).

  1. SQS Poll: receive_message(WaitTimeSeconds=20) — blocks for up to 20 seconds waiting for a message
  2. Receive: Deserialize job envelope (job_id, docs[], language, options)
  3. Process: For each document in the array, run the full translation pipeline (sentinel → Bedrock → validate → integrity check)
  4. Heartbeat: Every 5 minutes, extend the SQS message visibility timeout to prevent re-delivery during long-running documents
  5. Report: Each pair reports its events to POST /admin/translate/event/{job_id}, and the last worker of the job calls POST /admin/translate/complete/{job_id}
  6. Delete: Remove the message from SQS after successful processing
  7. Loop: Return to step 1

Auto-Scaling: Demand-Driven Container Management

BeastTranslate scales itself automatically — no manual intervention required. The system determines container count from the job's language list at submission time, then scales back to 1 when all work completes.

How It Determines Scale

When a translation job is submitted via POST /admin/translate, the LPO server inspects the langs array:

  1. Count languages: desired = len(langs) — one container per language for full parallelism
  2. Cap at 12: txlimits.MaxLangsPerQuote, the same constant that caps a quote, so a 12-language job never leaves its 12th language waiting for a free container
  3. Scale up: ecs:UpdateService(desiredCount=N) fires immediately after the job is enqueued to SQS
  4. Containers ready: New containers start from ECR image cache in ~30 seconds

How It Determines Scale-Down

When a worker completes a job, the finalize step checks for remaining work before scaling down:

  1. Query active queue: GET /admin/translate/queue — checks active and queued job counts
  2. If queue empty: ecs:UpdateService(desiredCount=1) — scale back to steady-state
  3. If jobs remain: Skip scale-down — let other workers continue processing

This handles back-to-back batch submissions (e.g., 6 docs + 6 docs) correctly — the first batch to finish won't tear down containers while the second is still running.

ScenarioContainersTrigger
Idle (no jobs)1Default steady-state — near-zero CPU
Small job (1–3 languages)1–3Auto-scaled at submission: desired = len(langs)
Full library (11 languages)11Auto-scaled at submission — full parallelism
Largest customer quote (12 languages)12Auto-scaled at accept — every language runs at once
Post-batch (all jobs done)1Auto-scaled down by finalize step when queue is empty

Zero operator involvement. Submit the job, walk away. The infrastructure sizes itself to the workload, processes at maximum parallelism, then shrinks back to 1 container (~$0.05/hour at idle). KCC manual commands (translate-scale N) still work as an override but are no longer needed for normal operation.

Graceful Shutdown (SIGTERM)

When ECS sends SIGTERM (scale-down or deployment), the container:

  1. Stops polling for new messages immediately
  2. Finishes translating the current document (never abandons mid-document)
  3. POSTs partial progress back via callback
  4. Exits cleanly with code 0

This ensures no work is lost during scale-down events. A partially-completed job resumes from where it left off when the next container picks up remaining messages.

Backward Compatibility

The worker runs in persistent polling mode, continuously polling SQS for translation messages. Each message = one language × all docs for that job. Auto-scaling provides parallelism across languages.

Diagram 5.2: BeastTranslate Auto-Scaling — Multi-Language Job Flow

sequenceDiagram
    participant Admin as Operator or Customer accept
    participant LPO as LPO Server
    participant ECS as ECS Service
    participant SQS as SQS Queue
    participant BT1 as BeastTranslate 1
    participant BT2 as BeastTranslate 2-12

    rect rgba(96, 165, 250, 0.14)
    Admin->>LPO: POST /admin/translate (3 docs x 12 langs)
    LPO->>LPO: Insert the job row before any send
    LPO->>SQS: 12 messages, one per language (each carries all 3 docs)
    LPO->>ECS: UpdateService(desiredCount=12)
    LPO->>Admin: 200 OK (job_id, state: queued)
    end

    Note over ECS: Spins up 11 new containers (~30s)
    Note over BT1: Already polling (always-on)
    BT1->>SQS: ReceiveMessage
    SQS-->>BT1: One language message (all 3 docs)
    BT2->>SQS: ReceiveMessage (competing consumers)
    SQS-->>BT2: One language message each

    rect rgba(251, 191, 36, 0.12)
    par 12 containers process in parallel
        BT1->>BT1: es - all 3 docs
        BT2->>BT2: fr, de, ru, ja, zh, ar, hi, ur, pt, it, ko
    end
    end

    BT1->>LPO: Per-pair events (lang_started, lang_succeeded, lang_failed)
    BT2->>LPO: Last worker: POST /admin/translate/complete/job_id
    rect rgba(52, 211, 153, 0.14)
    LPO->>LPO: Final state, search rebuild, queue check
    LPO->>ECS: UpdateService(desiredCount=1) once the queue is empty
    Note over ECS: 11 containers drain gracefully
    alt Customer job
        LPO->>Admin: Customer completion email, failed pairs go to the refund machine
    else Internal job
        LPO->>Admin: Ops SNS notification email
    end
    end
        

5.2 Error Handling and Recovery

Failure ModeHandlingJob State
Single language fails after 3 retriesCatch → RecordLangFailure pass state, continue other langspartial
All languages for a doc failDeploy Lambda receives empty succeeded list, skips invalidationpartial
Worker timeout (no response)ECS task runs to completion — no timeout ceiling. Step Function waits via ecs:runTask.syncrunning
Step Function execution exceptionFinalize still runs via catch-all; job marked failedfailed
Operator cancels mid-flightStopExecution API call; job marked cancelledcancelled
Step Function fails before FinalizeSelf-healing sweeper detects orphaned job via execution ARN, marks as failedfailed
A pair fails on a customer jobThe pair enters the failed-pair refund machine when the job ends: one free retry, then an automatic refund for what is still undelivered. lang_rejected counts as failed for customer jobspartial / failed

Per-lang independence: Failure of one (doc, lang) pair never aborts work on the other 10 languages. This is enforced by the Step Function's Catch on the inner Map iterator — errors are captured as data, not propagated as exceptions.

5.3 Architecture Simplification (June 2026)

Simplified June 27, 2026. The EventBridge Pipe, Step Function (tbi-translation-orchestrator), and all translation Lambdas (tbi-translate-init, tbi-translate-deploy, tbi-translate-finalize) have been deleted. The pipeline is now: LPO server → N SQS messages (one per language) → N BeastTranslate containers → POST /admin/translate/complete/{job_id}. Zero orchestration layers. Full pipeline control from the container.

5.4 Self-Healing Sweeper

The sweeper runs automatically on every GET /admin/translate/health call (piggybacked) and is also available as a dedicated POST /admin/translate/sweep endpoint.

It scans all jobs in tx:active (the Valkey SET of active job IDs). For each job older than 15 minutes in queued or running state:

All sweep actions are logged to translation_job_events for audit trail.

The Second Sweeper: Customer Refunds and Stranded Jobs

A second sweeper keeps the promises made to paying customers. It is an LPO timer, tx_failed_refund_sweep, that ticks every 15 minutes (refund.sweep_interval_minutes) and takes a transaction-scoped advisory lock so only one container sweeps at a time. Its step 0 finishes any customer job nobody finalized (TX-FINALIZE-CATCHUP), claiming the job's finalizing phase as a 15-minute lease so a tick that dies is picked up by the next one. The later steps close retry windows, issue refunds that are due, and refund a paid quote whose job never started. Every step is a re-runnable conditional update in Aurora, so a restart never loses or repeats work, and POST /admin/translate/refund-sweep runs one tick on demand.

Result: This eliminates the stuck queue problem permanently — no manual cleanup needed. Jobs that silently fail are automatically detected and marked, keeping the active set accurate and the queue healthy.

5.5 Job Phase Transitions

The job state now reflects the exact phase of execution:

PhaseMeaning
queuedSubmitted to SQS, waiting for BeastTranslate to pick up the message (typically <20 seconds)
runningBeastTranslate received the message, worker translating documents
deployingLegacy phase of the retired deploy Lambda. No new job enters it; older rows that still carry it are finished by the catch-up exactly like running
finalizingThe last worker called POST /admin/translate/complete/{job_id}; the LPO gathers the results from Aurora, invalidates CloudFront for library documents, rebuilds the search index and sends the email. For a customer job nobody finalized, the 15-minute refund sweeper claims this phase as a 15-minute lease, so a tick that dies is picked up by the next one
succeeded / partial / failedTerminal states — all sub-tasks complete, email notification sent

This gives real-time visibility into exactly where a job is in the pipeline.

6. Admin API (9 Endpoints)

All endpoints require the X-Admin-Key header. They are served by the LPO server (Go) alongside the existing admin routes.

6.1 Submit Translation Job

POST /admin/translate

Submits a new translation job. Validates inputs, checks cost limits, creates job state in Valkey, inserts the translation_jobs row synchronously (on a detached 60-second context, and under the quote row lock for a customer quote job) before any SQS send, then enqueues one message per language. first_enqueued_at marks the first successful send, which is what makes a row count as started.

// Request
POST /admin/translate
X-Admin-Key: tbcc-admin-...
X-Idempotency-Key: my-unique-key (optional)
Content-Type: application/json

{
  "docs": ["Trinity-Beast-API-Reference.html", "Trinity-Beast-Architecture-Guide.html"],
  "langs": "all",
  "options": {
    "force": false,
    "delta": false,
    "skip_search_rebuild": false,
    "skip_validation": false
  }
}

// Response 200
{
  "status": "✅ [LPO] [us-east-2] [BeastMain] [/admin/translate] [200]",
  "status_code": 200,
  "endpoint": "/admin/translate",
  "cluster_node": "BeastMain",
  "region": "us-east-2",
  "language": "en",
  "timestamp": "2026-05-16T16:42:00Z",
  "data": {
    "job_id": "1747407720-a3f8b2c1d4e5",
    "state": "queued",
    "submitted_at": "2026-05-16T16:42:00Z"
  },
  "error": ""
}

Validation rules:

6.2 Monitoring Endpoints

GET /admin/translate/status/{job_id}

Returns the full job state. Aurora is the primary source — state, timestamps, docs, langs, cost, and Step Function ARN are read from translation_jobs. Real-time per-doc/lang progress is overlaid from Valkey (written per-pair by the worker, too frequent for Aurora writes). If Aurora doesn't have the job yet (async insert still pending), falls back to Valkey.

GET /admin/translate/queue

Lists all pending and active jobs (state in queued or running).

GET /admin/translate/history

Returns the last 50 completed jobs from translation_jobs in Aurora, ordered by submission date descending. Includes state, docs, succeeded/failed pair counts, cost, and reason. Falls back to the Valkey tx:history list if Aurora is unavailable.

GET /admin/translate/health

System health overview:

{
  "status": "✅ [LPO] [us-east-2] [BeastMain] [/admin/translate/health] [200]",
  "status_code": 200,
  "endpoint": "/admin/translate/health",
  "cluster_node": "BeastMain",
  "region": "us-east-2",
  "language": "en",
  "timestamp": "2026-05-16T17:30:00Z",
  "data": {
    "queue_depth": 0,
    "active_jobs": 1,
    "last_completed_at": "2026-05-16T17:30:00Z",
    "last_state": "succeeded",
    "daily_spend_usd": "12.40",
    "daily_spend_limit_usd": "600.00",
    "daily_input_tokens": 284150,
    "daily_output_tokens": 312480,
    "daily_token_limit": 50000000,
    "swept_jobs": 0
  },
  "error": ""
}

6.3 Control Endpoints

POST /admin/translate/cancel/{job_id}

Stops the Step Function execution via StopExecution API. Marks job as cancelled. Returns 409 if already in a terminal state.

POST /admin/translate/retry-failed/{job_id}

Creates a new job from the failed (doc, lang) pairs of a completed-with-partial job. Returns 409 if the original is still running. A retry runs on the same model as the original job. The handler walks retry_of back to the root job, so even a retry of a retry inherits the first job's model, and writes that model to the new translation_jobs row, the Valkey job hash and the queue message. Before this fix the model was in neither the message nor options, which is why inheriting options alone did not carry it; it now travels at the top level of the message and in options.model. A root job with no recorded model retries on Qwen 3 235B, our standard, never on a fallback.

Inherits the parent job's routing options as of 2026-09-06 — fixed after shipping a wrong deliverable. The handler used to build the retry's options from a bare {"force": true}, discarding everything else the parent job carried. For a customer job that includes quote_id, source_bucket/source_prefix, output_bucket/output_prefix, skip_validation, and execution_mode — the entire routing that sends a translation to the customer's own S3 prefix instead of the internal document library. A retry that dropped those routed the retried pair to the wrong bucket while still reporting succeeded. The handler now reads the parent job's options column from Aurora and inherits it wholesale, overriding only force.

Customer jobs: a retry of a customer job goes through ExecuteCustomerRetry, the same path the customer's Retry button uses. That means one free retry per quote, only the retryable failed pairs, the quote's own agent read from translation_quotes.model / model_by_lang, and translation_jobs.quote_id set on the retry job. An operator can never reopen a pair that was already refunded.

POST /admin/translate/refund-sweep

Runs one tick of the failed-pair refund sweeper (5.4) now. Returns {opened, closed_none, due, issued, failed, errors}; 409 translate_refund_sweep_busy when another container holds the lock, 503 translate_refund_sweep_failed when the sweep couldn't run.

POST /admin/translate/sweep

Manually triggers the self-healing sweeper. Idempotent — safe to call repeatedly.

// Response 200
{
  "status": "✅ [LPO] [us-east-2] [BeastMain] [/admin/translate/sweep] [200]",
  "status_code": 200,
  "endpoint": "/admin/translate/sweep",
  "cluster_node": "BeastMain",
  "region": "us-east-2",
  "language": "en",
  "timestamp": "2026-05-16T18:00:00Z",
  "data": {
    "swept": 2,
    "checked": 5,
    "results": [
      {
        "job_id": "1747407720-a3f8b2c1d4e5",
        "prior_state": "running",
        "submitted_at": "2026-05-16T16:42:00Z",
        "sfn_status": "FAILED",
        "action": "marked_failed"
      }
    ]
  },
  "error": ""
}

6.4 Worker Callback Endpoints

These endpoints are called by the BeastTranslate worker to update Aurora without needing direct database access (the worker runs outside the VPC). Until June 27, 2026 the finalize Lambda called them too; it was deleted with the Step Functions pipeline (5.3).

POST /admin/translate/update/{job_id}

Updates job state, progress, cost, and timing fields. Called by the worker after each (doc, lang) translation.

POST /admin/translate/event/{job_id}

Records a granular event in the translation_job_events table. Used for audit trail — each doc/lang start, success, failure, retry is logged as a separate event. Two new outcomes: lang_rejected with reason: "target_is_source" (the worker found, for certain, that the document is already in the target language; non-retryable, so the pair is refunded rather than retried), and lang_failed with error: "cancel-state-unknown" (a customer job's before-publish cancel check could not be answered, so the worker failed closed and published nothing).

Fire-and-forget pattern: Both callback endpoints always return 200 regardless of Aurora write outcome. The translation pipeline must never fail because observability data couldn't be written. Errors are logged but never propagated.

7. Aurora Observability — Source of Truth

Aurora is the authoritative record for all translation job state. Valkey serves one specific role: real-time per-pair progress updates during active execution (written too frequently for Aurora). For everything else — job state, history, cost, audit trail — Aurora is read first.

Design principle: Valkey is the price cache, search indexes, and real-time counters. It is not a job ledger. Aurora is the ledger. When you need to know what was translated, when, at what cost, and with what result — query Aurora.

7.1 translation_jobs Table

One row per job submission. 28 columns covering the full lifecycle. This table is the ground truth for gap analysis, cost reporting, and audit:

Column GroupFieldsPurpose
Identityid, job_id, idempotency_keyUnique identification and deduplication
Statestate, submitted_at, started_at, completed_atLifecycle tracking — authoritative terminal state
Inputdocs (JSONB), langs (JSONB), options (JSONB)What was requested
Progresstotal_pairs, succeeded_pairs, failed_pairs, progress (JSONB)Per-doc/lang status map
Costbedrock_cost_usd, bedrock_invocationsSpend tracking per job
Executionstep_function_arn, errors (JSONB), elapsed_secondsTraceability and debugging
Deploymentcloudfront_invalidation_ids, search_index_rebuilt, notification_sentPost-translation actions
Lineageretry_of, reasonRetry chain and submission reason
Metadatasubmitted_by, created_at, updated_atAudit trail

Gap analysis query: To find which documents have never been translated, query SELECT DISTINCT jsonb_array_elements_text(docs) FROM translation_jobs ORDER BY 1 and compare against the S3 document list. Aurora is the only reliable source for this — Valkey keys expire and don't persist across cache flushes.

7.2 translation_job_events Table

Granular audit log — one row per significant event in a job's lifecycle. Used by the retry-failed handler as the authoritative source of which (doc, lang) pairs failed:

ColumnTypeExample Values
job_idVARCHAR1747407720-a3f8b2c1d4e5
event_typeVARCHARlang_started, lang_succeeded, lang_failed, deploy_started, finalize_complete
docVARCHARTrinity-Beast-API-Reference.html
langVARCHARja, ar, es
detailJSONBCost, chunk count, error message, validator report
created_atTIMESTAMPEvent timestamp

7.3 Read/Write Strategy

The translation system uses a deliberate split between Aurora and Valkey based on access pattern:

DataPrimary StoreReason
Job state (queued/running/succeeded/failed)AuroraAuthoritative terminal state — never expires, queryable, auditable
Job history (last 50 completed)AuroraPermanent record — survives cache flushes, supports gap analysis
Per-pair progress (es: succeeded, ja: running…)ValkeyWritten per-pair during execution — too frequent for Aurora writes, only needed during active polling
Daily spend counterValkeyNeeds atomic INCRBYFLOAT and 24h TTL auto-reset — Aurora is wrong tool for this
Active job setValkeyFast set membership check on every submit — Aurora query would add latency to the hot path

Write path

Read path

Do not rely on Valkey for job state. Valkey keys have no TTL on job hashes and can be flushed, evicted under memory pressure, or simply stale if the finalize Lambda's update call was lost. Aurora is the record of what happened. Valkey is the window into what is happening right now.

8. Cost Protection

The translation engine calls Bedrock for every chunk of every document in every language, using the customer's chosen agent. Without guardrails, a single typo in a batch submission could trigger hundreds of expensive API calls.

8.1 Three Protection Layers

LayerWhereLimitBehavior on Breach
Per-request limitsAdmin API (submit handler)Max 42 docs, max 12 langs; active jobs are not capped (a warning is logged at 12 or more)400 Bad Request (docs/langs); every accepted job is queued in SQS
Daily dollar capAdmin API (submit handler)$600/day (autoops:bedrock:spend:daily)429 Too Many Requests until counter expires
Daily token capAdmin API (submit handler)50M combined tokens/day (autoops:bedrock:tokens:input:daily + autoops:bedrock:tokens:output:daily)429 Too Many Requests until counters expire
Per-invocation trackingWorker taskIncrements after every Bedrock callSource of truth for daily counters

8.2 Spend Tracking

Two parallel counters track daily usage — a dollar cap and a token cap. Both live in Valkey with 24-hour TTL auto-reset and are checked on every job submission.

Dollar Cap (autoops:bedrock:spend:daily)

Why $600? The cap is a daily safety guardrail, and on the standing model it is enormous headroom rather than a tight ceiling. The standing model is Qwen 3 235B at $0.22 per million input tokens and $0.88 per million output — roughly 15–19× cheaper per token than the Claude Sonnet tier. Measured against real completed jobs, a document-language pair costs on the order of one to two cents, so re-translating the entire library in one pass lands in the single-digit dollars. A full day of translation cannot realistically approach $600; what the cap actually protects against is a runaway loop or a mistakenly enormous submission, and it shares the window with normal AutoOps overhead (threat analysis, digests, support).

Do not quote a per-pair or per-library dollar figure from this page. Rates change and the model can be switched per language. Derive them: GET /admin/translate/params returns the live rates from the translation_parameters table, and translation_jobs holds bedrock_cost_usd and total_pairs for every real job, which is the only honest source for cost per pair. This paragraph replaced a stale claim of "$726 for a 40-document library at ~$1.65 per pair (Sonnet 4.6)" — figures from the Claude era that survived the move to Qwen and overstated the real cost by roughly two orders of magnitude, against a library that is now 44 documents. A number written into prose has no owner.

Token Cap (autoops:bedrock:tokens:input:daily + autoops:bedrock:tokens:output:daily)

⚠️ Which cap fires first depends entirely on the model, and on the standing model it is the TOKEN cap — not the dollar cap. This section previously read "At Sonnet 4.6 rates, 50M tokens ≈ $750 — safely above the $600 dollar cap, so the dollar cap fires first under normal conditions." That was true on Claude Sonnet. On Qwen 3 235B it inverts: 51M tokens at the live rates, using the configured output-to-input ratio, works out to roughly $26 of spend. The token cap therefore trips at about 4% of the dollar cap, and the $600 ceiling is effectively unreachable by translation alone.

Neither number is wrong — they simply answer different questions, and the ordering between them is a property of the current model rather than a fixed fact. Re-derive the ordering whenever the standing model changes: read the rates from translation_parameters, the two ceilings from controls.daily_spend_limit_usd and controls.daily_token_limit, and the mix from estimation.output_input_ratio. A stated ordering that nobody re-derived is how this paragraph came to describe a model we stopped using.

Kill switch: Setting autoops:bedrock:kill = "1" in Valkey causes both the submit endpoint and the worker task to refuse all operations. Use this for emergency cost containment.

Customer price — a flat $3.00 per document-language pair. This is what a customer pays, and it is deliberately simple: one document into one language is one pair at $3.00, so a customer can predict any quote by multiplying pairs × $3.00 without calling the API. The one exception is the Claude Opus 4.8 agent, where the premium model's token cost can exceed $3.00 on a large document; those pairs run the cost-plus calculation below with $3.00 as a floor and are advertised as "From $3.00". Every other agent is a true flat rate — the cost-plus math does not run for them at all.

Internal cost model (what we spend, not what the customer sees): the figures below are how TBI tracks its own cost and margin. They are never serialized to a customer quote — the quote endpoints return price only (price_per_pair, total_pairs, total_price, currency).

The engine is completely agent-agnostic. Any model accessible through Amazon Bedrock's invoke_model API can serve as a translation agent — whether it uses the Anthropic Messages format or the OpenAI-compatible format. The engine auto-detects the provider and constructs the appropriate request/response envelope. We selected these six agents based on three criteria: (1) quality on multilingual document translation, (2) availability across our failover regions (us-east-2, us-east-1, us-west-2), and (3) distinct cost/quality tradeoffs that let customers choose the right agent for their workload.

Token rates (stored in Aurora translation_parameters, cached in Valkey, refreshed every night from the AWS Price List):

The rates keep themselves honest. These numbers used to be typed in by hand, and they drifted: the stored Claude Sonnet 4.6 rates were the global cross-Region price while our us. inference profiles are billed at the geographic price, about 10% higher. Now the nightly sync job (BeastReconciler) reads every model's rates from the public AWS Price List (pricing:GetProducts, priced for us-east-2), checks each value, and writes any change to the bedrock_pricing rows in Aurora before it copies the table into Valkey tx:params; then it confirms both stores agree. It fails safe one model at a time: a fetch error, a missing value, an unknown unit or a jump of more than 3× in one night keeps that model's stored rates and raises a single ops alert, and nothing in it can stop the rest of the nightly job. Aurora stays the source of truth, so Valkey only ever receives what Aurora holds. For a deploy, SYNC_ONLY=bedrock-rates runs just the refresh, the copy and the check, without resetting the day's spend guards.

AgentTierInput/1MOutput/1MSpeedRegions
Qwen3 235B💰 Value$0.22$0.880.6×east-2, west-2
Mistral Large 3💰 Value$0.50$1.500.6×east-2, east-1, west-2
DeepSeek V3💰 Value$0.58$1.680.7×east-2, west-2
Claude Haiku 5.5⚡ Standard$0.11$0.550.5×All (cross-region)
Claude Sonnet 4.6🏆 Premium$3.30$16.501.0×All (cross-region)
Claude Opus 4.8👑 Elite$5.50$27.501.5×All (cross-region)

These are the us-east-2 rates the nightly refresh stored on October 10, 2026, the day the Claude agents moved to Haiku 5.5 and Opus 4.8 (rate keys haiku55, sonnet46, opus48). Sonnet 4.6 now carries the geographic rate it is actually billed at, 10% above the global rate the table used to show. Because the refresh runs every night, treat GET /admin/translate/params as the live answer. Speed factor is relative to Sonnet 4.6 (1.0×). Lower means faster — Haiku at 0.5× processes in half the time. The speed factor directly affects the duration-based infrastructure cost: faster agents cost less in compute time per pair.

Infrastructure cost formula (duration-based):

Regional failover: If the primary region (us-east-2) returns a timeout or a 5xx, the engine tries the call twice there, five seconds apart, then fails over to the next region in the chain (§12). Anthropic models use cross-region inference profiles (us. prefix) and route automatically. Qwen and DeepSeek fail over from east-2 to west-2. Mistral is available in all 3 regions.

Typical internal cost vs. customer price (per 50 KB document × 1 language, Express): the first two columns are what TBI spends; the last is what the customer pays. The gap between them is the margin that funds the mission — and the reason the price stays a flat, predictable number instead of a few volatile cents.

AgentBedrock (cost)Infra (cost)Customer Price
Qwen3 235B$0.02$0.01$3.00
Mistral Large 3$0.04$0.01$3.00
DeepSeek V3$0.04$0.01$3.00
Claude Haiku 5.5$0.01$0.01$3.00
Claude Sonnet 4.6$0.34$0.01$3.00
Claude Opus 4.8$0.57$0.01From $3.00

$3.00 per document-language pair — a true flat rate: every non-Opus agent is priced at a flat $3.00 per pair regardless of document size. The Bedrock and Infra columns above show what the work costs us, not what the customer pays; the cost-plus calculation does not run for these agents at all. Claude Opus 4.8 is the sole exception — its cost-plus total can climb past $3.00 on a large document, so it is priced "From $3.00". The flat rate exists because the value of the result is a professionally translated document, not the cost of the compute, and because a predictable price a customer can compute themselves is worth more to them than precision.

8.3 Infrastructure Integration

Translation engine metrics are exposed through two public interfaces:

Email notification timing: The email notification is the absolute LAST step in the pipeline. It fires only after: translation, deployment, search index rebuild, state update, and history push are ALL complete. The email is a comprehensive report including: job summary, translation results, CloudFront invalidation IDs, search index status, and any Bedrock error details. If Bedrock reports validation failures, the specific error messages and validator details are included in the email.

9. CLI Compatibility

The existing CLI tool (scripts/kcc_helpers/translate_doc.py) continues to work unchanged. A --remote flag routes through the new service instead of running Bedrock locally:

FlagBehaviorUse Case
--local (current default)Runs translator engine in-process, calls Bedrock directly from laptopDevelopment, debugging, single-doc quick fixes
--remotePOSTs to /admin/translate, polls /admin/translate/status/{id} every 5s, streams progress to stdoutProduction translations, batch operations

The --remote flag produces identical terminal output to local mode — same progress bars, same chunk counters, same completion summary. The operator's workflow doesn't change; only the execution path does.

Default flip plan: Start with --local as default to avoid surprising anyone. After 30 days of clean production runs through the service, flip the default to --remote and add --local as the explicit fallback.

10. Configuration Reference — Protected Terms

All translation behavior is driven by a single config file: scripts/translation-config.json. This is the shared source of truth consumed by both the Python engine and the Go admin API.

10.1 Protected Terms (60 entries)

Brand names, product names, AWS services, exchange names, and acronyms that must never be translated or transliterated:

Cross Power Ministries of Pakistan, The Trinity Beast Infrastructure,
The Trinity Beast, Trinity Beast Command Center, Kiro Command Center,
Cory Dean Kalani, Shafiq Bhatti, BeastWebhook, BeastMirror, BeastMain,
BeastLRS, Claude Sonnet 4.6, Bedrock, ElastiCache, EventBridge,
CloudFront, GuardDuty, CloudWatch, CloudTrail, Step Functions,
Crypto.com, Coinbase, Gate.io, Bitstamp, Kraken, Aurora, Valkey,
Stripe, Kiro, Fargate, PostgreSQL, Lambda, Route 53, AutoOps,
TBCC, CPMP, TBI, KCC, OKX, ECR, ECS, ALB, NLB, WAF, SNS, SQS,
SES, VPC, IAM, S3 ...

Per-Request Protected Terms

In addition to the global protected terms list, you can submit document-specific terms via the protected_terms array in the translation request. This is useful for:

POST /admin/translate
{
  "docs": ["Trinity-Beast-API-Reference.html"],
  "langs": "all",
  "protected_terms": ["MyCustomService", "SpecialEndpoint", "ProjectAlpha"]
}

Per-request terms are merged with the global list for that job only. They do not persist across jobs.

10b. Configuration Reference — Preserve Patterns

10.2 Preserve Patterns

Regex patterns for technical tokens that must survive translation unchanged:

Pattern NameMatchesExample
urlHTTP/HTTPS URLshttps://api.cpmp-site.org/admin/translate
emailEmail addressesCoryDeanKalani@CPMP-Site.org
memory_sizeNumber + memory unit1770 MB, 32 GB
percentageNumber + %98.5%, 62%
cron_exprCron expressionscron(0 11 * * ? *)
ip_addressIPv4 with optional CIDR10.0.1.0/24
aws_arnAWS ARN formatarn:aws:sns:us-east-2:211998422884:tbi-ops-notifications
aws_resource_idAWS resource identifiersvpc-03deaddb7083cd59c, sg-050b617f93b2388f6

10c. Configuration Reference — Limits

10.3 Limits

ParameterValuePurpose
max_chunk_chars6000Default maximum characters per chunk (Latin scripts: es, pt, fr, de)
max_chunk_chars_by_langSee belowPer-language overrides for complex scripts
max_retries3Retry attempts per chunk on validation failure
request_timeout_seconds300Per-Bedrock-call timeout (5 minutes — large RTL chunks need headroom)
max_output_tokens8192Maximum tokens in Bedrock response

Per-language chunk size overrides:

LanguagesChunk SizeRationale
hi, ur, ar3000 charsDevanagari and Arabic scripts expand significantly during translation. Smaller chunks prevent Bedrock timeouts.
ja, zh, ru4500 charsCJK and Cyrillic have moderate expansion. Mid-range chunks balance throughput and reliability.
es, pt, fr, de, it6000 chars (default)Latin scripts translate quickly with minimal expansion.

Every catalog language inherits sensible limits. The table above covers our library's 11 languages. Each of the 127 catalog languages resolves its limits by exact code, then its base_lang, then its tuning_lang, then the default. So fa, ps and sd inherit Urdu's tuning, ko inherits Japanese, Cyrillic languages inherit Russian, and a regional variant such as es-MX follows its base language. The tuning_lang column of the catalog decides it, so a new language is tuned the day it is added.

11. Operations Guide

11.1 Submitting a Translation Job

Single document, all languages:

curl -s -X POST \
  -H "X-Admin-Key: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"docs":["Trinity-Beast-API-Reference.html"],"langs":"all"}' \
  https://api.cpmp-site.org/admin/translate | jq .

Multiple documents, specific languages:

curl -s -X POST \
  -H "X-Admin-Key: $ADMIN_KEY" \
  -H "Content-Type: application/json" \
  -d '{"docs":["Trinity-Beast-API-Reference.html","Trinity-Beast-Architecture-Guide.html"],"langs":["es","pt","fr"]}' \
  https://api.cpmp-site.org/admin/translate | jq .

With idempotency key (safe to retry):

curl -s -X POST \
  -H "X-Admin-Key: $ADMIN_KEY" \
  -H "X-Idempotency-Key: api-ref-2026-05-16" \
  -H "Content-Type: application/json" \
  -d '{"docs":["Trinity-Beast-API-Reference.html"],"langs":"all"}' \
  https://api.cpmp-site.org/admin/translate | jq .

11.2 Monitoring Progress

# Check job status
curl -s -H "X-Admin-Key: $ADMIN_KEY" \
  https://api.cpmp-site.org/admin/translate/status/{job_id} | jq .

# View queue
curl -s -H "X-Admin-Key: $ADMIN_KEY" \
  https://api.cpmp-site.org/admin/translate/queue | jq .

# System health
curl -s -H "X-Admin-Key: $ADMIN_KEY" \
  https://api.cpmp-site.org/admin/translate/health | jq .

# Recent history
curl -s -H "X-Admin-Key: $ADMIN_KEY" \
  https://api.cpmp-site.org/admin/translate/history | jq .

11.3 Troubleshooting

SymptomCauseResolution
Job stuck in queuedEventBridge Pipe not consumingCheck Pipe status in console; verify IAM role
429 on submitDaily spend cap hit ($600)Wait for 24h TTL expiry, or reset manually: SET autoops:bedrock:spend:daily 0
Partial completionSome languages failed validationPOST /admin/translate/retry-failed/{id}
Worker timeoutDocument too large (many chunks)Check Step Function execution history for the failing chunk index
Cancel returns 404Job only in Aurora, not ValkeyCancel handler falls back to Aurora — ensure latest code is deployed
No email notificationComplete endpoint error or SNS failureCheck LPO container logs for TRANSLATE-COMPLETE entries; verify SNS topic tbi-ops-notifications
Search not updatedSearch rebuild timed outRun bash scripts/kcc.sh build-search manually

Cancel a running job:

curl -s -X POST -H "X-Admin-Key: $ADMIN_KEY" \
  https://api.cpmp-site.org/admin/translate/cancel/{job_id}

This stops the Step Function execution immediately. Documents already translated and deployed remain live. The search index is rebuilt for whatever landed successfully.

12. Regional Failover

The translation engine implements automatic regional failover to maintain availability during Bedrock service disruptions. This was added after a us-east-2 outage during development exposed the single-region weakness — better to discover this before customers were affected.

12.1 Failover Chain

When a Bedrock call fails with a service-level error, the engine automatically retries in the next region:

PriorityRegionLocationRole
1us-east-2OhioPrimary — all normal traffic
2us-east-1N. VirginiaFirst fallback
3us-west-2OregonSecond fallback

The failover is transparent to the caller — the translation completes successfully as long as at least one region is available. A log message records when a fallback region was used.

12.2 Trigger Conditions

Failover is triggered for service-level errors and timeouts that indicate the region is unavailable or overloaded:

Error TypeMeaningAction
ServiceUnavailableExceptionBedrock service is down (503)Retry same region once, then failover
ThrottlingExceptionRate limit or capacity exceededRetry same region once, then failover
ModelStreamErrorExceptionModel streaming failureRetry same region once, then failover
ReadTimeoutErrorNo response within the call's read timeout, sized from its output cap (at most 900s; §4.2)Retry same region once, then failover
ConnectTimeoutErrorCould not establish connection within 10sRetry same region once, then failover
InternalServerException, ModelNotReadyException, HTTP 500/502/503/504Bedrock internal error or model not yet loadedRetry same region once, then failover
EndpointConnectionError, ConnectionClosedErrorNetwork path to the region failed mid-callRetry same region once, then failover

Other errors (validation failures, authentication errors, malformed requests) are not retried — they would fail identically everywhere.

Per-Region Retry with Backoff

Each region gets 2 attempts before the engine moves to the next region. A 5-second backoff between attempts allows transient pressure to clear:

us-east-2 (attempt 1) → timeout → wait 5s →
us-east-2 (attempt 2) → timeout →
us-east-1 (attempt 1) → timeout → wait 5s →
us-east-1 (attempt 2) → timeout →
us-west-2 (attempt 1) → timeout → wait 5s →
us-west-2 (attempt 2) → timeout → FAIL (raise exception)

Total: 6 attempts across 3 regions. In practice, transient spikes clear within 5-10 seconds, so the retry within the same region usually succeeds without needing failover.

Why the chain never includes a Canadian region. The Trinity Beast has a hard rule that no service, Lambda, or Bedrock call ever runs in ca-central-1 or ca-west-1 — no exceptions. This is enforced by construction, not by a runtime check: FAILOVER_REGIONS is a fixed allow-list containing only the three regions in the table above, so a Canadian region simply cannot be selected. An import-time assertion in the same module raises a fatal error if any configured region ever starts with ca-, so a misconfiguration is caught at worker startup rather than discovered mid-failover. The engine has never had a function that redirects a Canadian region to us-east-2 at request time — the earlier belief that one existed described a mechanism the codebase does not contain; the real mechanism is that the list was simply never given anywhere to redirect from.

12.3 Cost Impact

Regional failover has negligible cost impact:

Resilience benefit: A complete regional outage no longer blocks translations. The May 2026 us-east-2 outage would have caused a 4-hour translation blackout without this feature. With failover, translations continued uninterrupted via us-east-1.

13. Document Preparation Guide

Proper document preparation ensures clean translations with minimal post-processing. This section covers the conventions that help the translation engine produce accurate results.

13.1 Code Tag Usage

The <code translate="no"> tag tells the translation engine to preserve content exactly as written. Use it correctly to avoid formatting artifacts in translated documents.

When to Use Code Tags

Use <code translate="no"> for technical identifiers that would break if translated:

When NOT to Use Code Tags

Do not wrap pure data values in code tags — they should appear as plain text:

Why this matters: The translation engine's sentinel system protects code-tagged content from translation. If you wrap "32 GB" in code tags, it survives translation — but so does the monospace formatting, which looks wrong in prose. The engine has a post-processor that strips spurious code wrappers from pure numeric values, but it's better to author correctly from the start.

Quick Test

Ask yourself: "If I changed this value, would the system break?" If yes, use code tags. If no (it's just a number or measurement), leave it as plain text.

ContentWould changing it break something?Use code tags?
tbi-ops-notifyYes — Lambda name✅ Yes
1770 MBNo — just a memory size❌ No
/admin/translateYes — API endpoint✅ Yes
$600No — just a dollar amount❌ No
max_retriesYes — config key✅ Yes
3 retriesNo — just a count❌ No

13.2 Protected Terms Submission

For documents with domain-specific terminology not in the global protected terms list, submit additional terms with the translation request:

POST /admin/translate
{
  "docs": ["Customer-Integration-Guide.html"],
  "langs": "all",
  "protected_terms": [
    "CustomerCorp",
    "ProjectPhoenix",
    "DataSync API",
    "IntegrationHub"
  ]
}

These terms are added to the global list for this job only. The engine will:

  1. Wrap each term in <span translate="no"> during preprocessing
  2. Replace with sentinel tokens before sending to Bedrock
  3. Restore the original terms after translation
  4. Validate that all terms survived intact

Best Practices for Protected Terms

13.3 Clarification Workflow

When the translation engine encounters ambiguous content, it may flag it for human review. This happens in the validation phase when:

Flagged content appears in the job status response under the warnings array:

{
  "status": "✅ [LPO] [us-east-2] [BeastMain] [/admin/translate/status/1747407720-a3f8b2c1d4e5] [200]",
  "status_code": 200,
  "endpoint": "/admin/translate/status/1747407720-a3f8b2c1d4e5",
  "cluster_node": "BeastMain",
  "region": "us-east-2",
  "language": "en",
  "timestamp": "2026-05-16T17:45:00Z",
  "data": {
    "job_id": "1747407720-a3f8b2c1d4e5",
    "state": "succeeded",
    "warnings": [
      "chunk 14 (ja): soft failure — protected term 'DataSync' may have been altered",
      "chunk 22 (ar): soft failure — version number format changed from X.Y.Z to X.Y"
    ]
  },
  "error": ""
}

Soft failures don't block the translation — the output is still deployed. Review the warnings and manually verify the flagged sections if needed.

Feedback loop: If you consistently see the same term flagged, add it to the global protected terms list in scripts/translation-config.json. This prevents future warnings and improves translation quality across all documents.

14. Pre-Scan Complexity Analysis

Before translation begins, the engine analyzes each document for complexity factors that may cause validation failures. This pre-scan identifies code-heavy sections and recommends whether to proceed, exercise caution, or split the document.

14.1 Complexity Metrics

The pre-scan calculates a complexity score for each section based on:

FactorWeightWhy It Matters
Code tags1.0 per tagEach code tag must survive translation intact — more tags = more validation points
Code tags in tables1.5 per tagTables with code examples are harder — model tends to merge or drop tags when reordering
Tables2.0 per tableTables with technical content require careful structure preservation
Pre blocks0.5 per blockUsually have translate="no" — lower risk but still tracked
Protected spans0.3 per spanHandled by sentinel system — low risk

Section Thresholds

14.2 Recommendations

Based on the analysis, the pre-scan returns one of three recommendations:

RecommendationCriteriaAction
PROCEEDScore < 20, no high-density sectionsTranslate normally — low failure risk
CAUTIONScore < 50, ≤ 2 high-density sectionsProceed but monitor — may need retries
SPLITScore ≥ 50 OR > 3 high-density sectionsConsider splitting document before translation

Pre-Scan Output Example

DOCUMENT TRANSLATION COMPLEXITY ANALYSIS
========================================
Total characters: 81,107
Total sections: 13
Total code tags: 287
Overall complexity score: 415.4
Recommendation: SPLIT

WARNINGS:
  ⚠️  Document has 287 code tags — high validation failure risk
  ⚠️  Section 'step-function' has 51 code tags — consider simplifying
  ⚠️  Section 'observability' has 48 code tags — consider simplifying

HIGH-DENSITY SECTIONS (9):
  • architecture: 11 code tags, score 22.7
  • sentinel-system: 22 code tags, score 34.5
  • step-function: 51 code tags, score 71.1
  ...

SUGGESTED SPLIT: 4 parts
  → Split after 'validators' (After 3 high-density sections)
  → Split after 'observability' (After 3 high-density sections)
  → Split after 'doc-prep' (After 3 high-density sections)

14.3 Document Splitting

When the pre-scan recommends splitting, it suggests natural break points at section boundaries. Options for handling complex documents:

Option 1: Split into Multiple Documents

Create separate HTML files for each part (e.g., Doc-Part1.html, Doc-Part2.html). Each part translates independently with lower failure risk. Link them together with navigation.

Option 2: Simplify High-Density Sections

Reduce code tag density in problematic sections:

Option 3: Translate in Batches

Submit fewer languages per job (e.g., 3 instead of 11). This reduces concurrent load and allows the model more capacity per translation. Retry failed languages individually.

Per-Language Split Thresholds (v2.5)

Complex scripts (Urdu, Arabic, Hindi) struggle with high tag density even when Latin-script languages handle the same chunk fine. The prescan now applies per-language code tag limits — tighter thresholds for scripts where the model is more likely to drop markup:

LanguageScriptMax Code Tags per Part
Default (Latin, CJK, Cyrillic)Latin / Kanji / Cyrillic30
Urdu (ur)Nastaliq18
Arabic (ar)Arabic18
Hindi (hi)Devanagari20

Configuration key: max_code_tags_per_part_by_lang in translation-config.json. When the prescan runs for a specific language, it uses that language's threshold to determine split points. A document that translates as one part for Spanish may automatically split into 2-3 parts for Urdu.

Result: The Translation Service document (22 code tags in the Architecture section) previously failed for Urdu on every attempt. With the per-language threshold of 18, the prescan splits Architecture and Observability into separate parts. All 11 languages now translate successfully.

This document is an edge case: The Translation Engine documentation itself has 287 code tags and a complexity score of 415 — it's documentation about a translation engine, so it's packed with code examples. Most documents score under 50.

14.4 Splitting Safety Valve (v2.8)

Even when code tag density is low, a single part that exceeds the model's effective output window will be silently truncated — sections at the end of the part simply disappear from the output. The safety valve enforces a hard character limit per part regardless of prescan recommendations.

The Problem

The Performance Report (75 KB) has 18 sections with moderate code density. The prescan recommended splitting into 3 parts based on code tag thresholds. But Part 1 was 36 KB of prose-heavy content — well under the code tag limit but far beyond the model's output token budget. The model translated the first ~24 KB faithfully, then its output simply stopped. Sections 7-8 (partner-sustained, udp-engine) vanished without any error signal.

The Fix

# Safety valve: max chars per part (prevents model output truncation)
MAX_CHARS_PER_PART = 24000  # ~6000 tokens, well within max_output_tokens

The splitter now enforces a 24 KB ceiling on every part. If a part exceeds this limit after the prescan-based split, it is further subdivided at the nearest section boundary. This is conservative — Latin scripts could handle ~30 KB, but 24 KB is safe for all languages including RTL and CJK where token efficiency is lower.

Impact

DocumentBefore (v2.6)After (v2.8)
Performance Report (75 KB)3 parts (Part 1: 36 KB — truncated)4 parts (largest: 22 KB — clean)
API Reference (180 KB)8 parts (all under 24 KB already)8 parts (no change — already safe)
Translation Engine (116 KB)11 parts (code-density driven)11 parts (no change — code splits dominate)

The safety valve only activates when the prescan's code-tag-based splitting produces oversized parts. For most documents, the code density split already keeps parts well under 24 KB.

Result: Performance Report went from dropping 3 entire sections (silent truncation) to a perfect 18/18 sections, 4/4 diagrams, 20/20 <br/> tags across all 11 languages.

15. Document-Level Preprocessor

The document-level preprocessor is a critical layer that runs before chunking. It extracts complex HTML elements from the entire document, replacing them with simple Unicode placeholders. After translation, the postprocessor restores the original elements. This eliminates the "model drops tags" failure mode entirely.

15.1 The Problem

The per-chunk sentinel system (Section 3) works well for most documents, but complex documents with many <code>, <strong>, and <em> tags exposed a fundamental limitation:

Example failure: A chunk with 27 <code translate="no"> tags consistently failed validation with tag count mismatch (27→23) — the model dropped 4 placeholders despite explicit instructions to preserve them.

15.2 The Solution

Extract ALL problematic elements from the entire document before chunking. The model never sees these elements — only simple Unicode placeholders that it cannot confuse with HTML structure.

Key insight: The model cannot corrupt what it never sees. By extracting elements at the document level, each chunk has zero complex tags to worry about. The model translates clean prose with obvious markers.

Before vs After

Pipeline StageBefore (v2.2)After (v2.3)
Document received290 code tags290 code tags
After preprocessing—0 code tags (290 placeholders)
Per-chunk sentinels20+ placeholders per chunk0-2 placeholders per chunk
Model cognitive loadHigh (complex structure)Low (clean prose)
Validation failuresFrequent on complex docsRare

15.3 Processing Flow

The preprocessor uses a two-phase extraction model that integrates into the translation pipeline as the first step:

Document → PHASE 1 (Density Lift) → PHASE 2 (Individual Extract) → Chunk → Translate → Reassemble → POSTPROCESS → Output
              ↓                           ↓                                                              ↓
     Lift entire dense containers  Extract remaining code/pre/       Single-pass flat restore
     as [QZ_ZK_NNN] placeholders   strong/em/numeric individually    (no nesting, no iteration)
     Original HTML preserved        Build manifest mapping            from manifest

Phase 1 runs FIRST on the raw DOM. It inspects container elements (tr, li, dd, dt, p, div) and lifts any container whose ratio of protected elements to prose characters exceeds a per-language density threshold. The lifted BLOCK entries store the original HTML byte-for-byte — no nested placeholders inside them.

Phase 2 then runs on whatever Phase 1 didn't lift, extracting individual elements (pre, code, spans, strong/em, a-tags, numeric patterns) the same way as before.

Integration in engine.py

def translate(text, target_lang, mode="html", ...):
    # Build preprocessor config from language profile
    preprocess_config = {"script_family": profile.get("script_family", "latin")}
    
    # Step 1: PREPROCESS — Phase 1 lifts dense blocks, Phase 2 extracts individuals
    simplified_html, manifest = preprocess_for_translation(text, preprocess_config)
    
    # Step 2: CHUNK — Split simplified document
    head, chunks, tail = chunker.split_document(simplified_html, lang=target_lang)
    
    # Step 3: TRANSLATE — Each chunk through Bedrock (per-chunk sentinels still run)
    for chunk in chunks:
        translated = _translate_chunk(chunk, ...)
    
    # Step 4: REASSEMBLE
    reassembled = chunker.reassemble(head, translated_chunks, tail)
    
    # Step 5: POSTPROCESS — Single-pass flat restore (no nesting)
    output = postprocess_translation(reassembled, manifest)

15.4 Element Extraction

The preprocessor extracts elements in a two-phase order. Phase 1 (density lift) runs first on the raw DOM. Phase 2 (individual extraction) runs on whatever Phase 1 didn't lift, processing elements in order of specificity (most specific first) to handle nesting correctly:

PhasePassElements ExtractedPlaceholder Format
1Density liftEntire container elements (tr, li, dd, dt, p, div) exceeding density threshold[QZ_ZK_001]
21<pre translate="no"> blocks[QZ_ZP_001]
22<code translate="no"> tags[QZ_ZC_001]
23Other translate="no" elements[QZ_ZS_001]
23b<span class="..."> (structural CSS)[QZ_ZS_001]
24<strong>, <em>, <b>, <i> tags (short content ≤40 chars)[QZ_ZB_001], [QZ_ZI_001]
25<a> tags with event handlers[QZ_ZL_001]
26Numeric patterns (memory sizes, percentages, versions)[QZ_ZM_001], [QZ_ZN_001], [QZ_ZV_001]

Placeholder Format

Placeholders use ASCII square brackets with opaque Z-prefixed type codes. Three previous formats were evaluated and discarded:

The production format uses a two-letter nonsense prefix (QZ) and single-letter opaque type codes that carry no HTML or programming semantics:

Z-CodeElement TypeExample Placeholder
ZC<code> tags[QZ_ZC_000]
ZP<pre> blocks[QZ_ZP_001]
ZS<span> elements[QZ_ZS_014]
ZB<strong> / <b> tags[QZ_ZB_021]
ZI<em> / <i> tags[QZ_ZI_003]
ZL<a> links[QZ_ZL_007]
ZTType D tables[QZ_ZT_000]
ZKDense blocks (Phase 1 lift)[QZ_ZK_002]
ZQTable cells[QZ_ZQ_005]
ZWBrand terms[QZ_ZW_033]
ZXHTML comments[QZ_ZX_000]
ZEEmail addresses[QZ_ZE_001]
ZUBare URLs[QZ_ZU_002]
ZMMemory sizes (32 GB)[QZ_ZM_042]
ZNPercentages (98.5%)[QZ_ZN_008]
ZVVersion numbers (17.7)[QZ_ZV_004]

The result: 100% tag preservation across all models and all target languages. The model sees [QZ_ZC_000] and has zero semantic interpretation — it passes the token through unchanged. Postprocessing restores the original HTML from the manifest.

Phase 2 Nested Element Handling

Within Phase 2, the preprocessor handles arbitrary nesting depth by processing innermost elements first. Note: this nesting only occurs for individually-extracted elements — BLOCK entries from Phase 1 are always flat (they contain original HTML, never placeholders).

Source:
<span translate="no"><code translate="no">tbi-ops-notify</code> Lambda</span>

Pass 1: Extract inner code tag
<span translate="no">⟦CODE_001⟧ Lambda</span>

Pass 2: Extract outer span
⟦SPAN_002⟧

Model sees: ⟦SPAN_002⟧ (one token, no nesting)

Sibling Placeholder Awareness

When the preprocessor extracts elements from a container (e.g., a table cell), earlier passes leave placeholder text in the parent. Later passes must not be confused by these sibling placeholders — a <code translate="no"> tag in the same table cell as an already-extracted element is still a valid extraction target.

Bug fixed (v2.4): The original _is_inside_placeholder check walked up the DOM tree looking for the ⟦ character in any parent's text. This caused false positives — if a sibling element had been extracted (leaving ⟦CODE_042⟧ in the parent's text), the check incorrectly skipped remaining <code translate="no"> tags in the same container. Those unextracted tags then overwhelmed the model during complex-script translation (Hindi, Urdu). Fix: the check now always returns false — if an element still exists in the DOM tree, it wasn't extracted and is a valid target.

15.4a Density-Based Block Lifting

Phase 1 of the preprocessor inspects the raw DOM before any individual extraction. It identifies container elements where the ratio of protected elements to prose characters is too high — meaning there's too little translatable text to justify sending the element through the model.

Density Formula

density = protected_elements / max(prose_chars, 1)

Where:

The check uses pure DOM element counting — no regex. This makes it deterministic and immune to content patterns that could fool regex-based approaches.

Thresholds by Script Family

Thresholds are per-language via the script_family column in the translation_language_profiles Aurora table:

Script FamilyLanguagesDensity Threshold
latines, fr, de, pt, it0.06
cyrillicru0.05
cjkja, zh0.03
indichi, ur0.03
arabicar0.03

Guard Rails

Aurora Configuration Columns

The translation_language_profiles table stores per-language density configuration:

ColumnTypePurpose
script_familyvarcharSelects density threshold (latin, cyrillic, cjk, indic, arabic)
density_lift_thresholdnumericOverride threshold for this language (NULL = use script_family default)
density_max_proseintegerMax prose chars for lift eligibility (default 300)

Example: Endpoint Table Row

<tr>
  <td><span translate="no">GET</span></td>
  <td><code translate="no">/health</code></td>
  <td>LPO server health check</td>
</tr>

Protected elements: 2 (span + code)
Prose chars: 25 ("GET" + "/health" excluded, "LPO server health check" counted)
Density: 2 / 25 = 0.08

→ CJK (threshold 0.03): LIFTED as ⟦BLOCK_NNN⟧ — density 0.08 > 0.03
→ Latin (threshold 0.06): LIFTED as ⟦BLOCK_NNN⟧ — density 0.08 > 0.06
→ If prose were 40+ chars: density drops to 0.05, Latin would KEEP it

Results

ElastiCache doc (ja): 45 blocks lifted, 156/156 code tags survive translation intact. Zero retries.

KCC doc (ja): 82 blocks lifted, 327/327 code tags survive translation intact. Zero retries.

Block lifting eliminates the failure mode where code-heavy table rows overwhelm the model's attention. The model never sees these rows — it translates a single ⟦BLOCK_NNN⟧ token (which it passes through unchanged) instead of a complex structure with multiple inline placeholders.

15.5 Restoration

After translation, the postprocessor restores placeholders in a single flat pass in reverse index order (high → low) to prevent prefix collisions. Because Phase 1 (density lift) runs BEFORE Phase 2 (individual extraction), BLOCK entries contain original HTML — never nested placeholders.

Why Single-Pass Works

Translated output contains: ⟦BLOCK_003⟧ ... ⟦SPAN_002⟧ ... ⟦CODE_001⟧

Single pass (reverse order):
  Restore ⟦BLOCK_003⟧ → original HTML (no further substitution needed inside)
  Restore ⟦SPAN_002⟧ → <span translate="no">⟦CODE_001⟧ Lambda</span>
  Restore ⟦CODE_001⟧ → <code translate="no">tbi-ops-notify</code>

Final:
  <tr><td>...original row...</td></tr>
  <span translate="no"><code translate="no">tbi-ops-notify</code> Lambda</span>
  <code translate="no">tbi-ops-notify</code>

Perfect reconstruction — no nesting loops, no iteration.

Manifest Structure

The manifest maps each placeholder to its original HTML, enabling exact restoration:

{
  "⟦BLOCK_003⟧": {
    "type": "BLOCK",
    "html": "<tr><td><span translate=\"no\">GET</span></td><td><code translate=\"no\">/health</code></td><td>LPO server health check</td></tr>",
    "index": 3
  },
  "⟦SPAN_002⟧": {
    "type": "SPAN",
    "html": "<span translate=\"no\">⟦CODE_001⟧ Lambda</span>",
    "index": 2
  },
  "⟦CODE_001⟧": {
    "type": "CODE",
    "html": "<code translate=\"no\">tbi-ops-notify</code>",
    "index": 1
  }
}

Note: BLOCK entries always store pristine HTML (no ⟦...⟧ tokens inside). Phase 2 entries like SPAN may contain lower-index placeholders from inner elements extracted in a later pass — this is handled naturally by the reverse-order restore.

Result: The Translation Engine document (290 code tags, complexity 423) now translates with 0 retries across all 11 parts. Previously it failed consistently on Part 8 (config section with 27 code tags). Restoration is now a single deterministic pass with zero edge cases.

15.6 Numeric Pattern Extraction

Pass 5 extracts numeric patterns from the text after HTML element extraction. This protects bare numbers in prose that weren't already inside code or span tags. The model cannot convert, localize, or drop what it never sees.

Why Numeric Extraction Matters

When translating to complex scripts (Arabic, Hindi, Urdu), the model occasionally:

These transformations break technical accuracy. The numeric extraction pass prevents all of them.

Patterns Extracted

Pattern TypeRegexExamplesPlaceholder
Memory sizes\d+(?:\.\d+)?\s?(?:GB|MB|KB|TB)32 GB, 1770 MB, 256 KB⟦MEM_001⟧
Percentages\d+(?:\.\d+)?%98.5%, 62%, 100%⟦PCT_001⟧
Version numbers\d+\.\d+(?:\.\d+)?4.6, 17.7, 2.3.1⟦VER_001⟧

Processing Order

Numeric extraction runs after HTML element extraction (Passes 1-4). This means:

Example: Hindi Translation

Source:
"The Lambda uses 1770 MB of memory and achieves 98.5% uptime."

After Pass 5:
"The Lambda uses ⟦MEM_042⟧ of memory and achieves ⟦PCT_043⟧ uptime."

Model translates prose, placeholders survive intact.

After restoration:
"लैम्ब्डा 1770 MB मेमोरी का उपयोग करता है और 98.5% अपटाइम प्राप्त करता है।"

Technical values preserved exactly — no localization, no conversion.

Result: Translation failures caused by numeric value loss (preserve_memory_size: missing: GB, MB) are now resolved across all 11 languages. Numeric values survive intact regardless of target script.

Placeholder Collision Prevention

The numeric extraction pass includes safeguards to prevent extracting numbers that are part of existing placeholder names (e.g., the "001" in ⟦CODE_001⟧):

Without these guards, the numeric regex would corrupt placeholder names by extracting their index numbers, producing nested placeholders like ⟦CODE___TBN10__⟧ that the model cannot handle.

15.7 Type D — Table Micro-Translation

Technical documentation is dominated by tables where most columns are technical identifiers (column names, data types, endpoints, parameter keys) and only one or two columns contain translatable prose (descriptions, explanations). Sending these tables through the full translation pipeline is wasteful and risky — the model tends to transliterate technical values or corrupt table structure.

Type D solves this with a surgical approach: extract the entire table before translation, then micro-translate only the declared prose columns via a separate, targeted Bedrock call. The table structure is never modified — only cell text content changes.

HTML Markup

Document authors declare the translation contract directly on the <table> element using two attributes:

AttributeValuesEffect
data-tx-headers"all" | "none" | "0,2,4"Which header cells (<th>) to translate. "all" translates every header; "none" leaves all English; comma-separated indices target specific columns.
data-tx-cols"" | "4" | "0,1,2"Which body cell columns (<td>) to translate. Empty string means no body cells. Comma-separated zero-based column indices target specific columns.

Example: Data Dictionary Table

<table data-tx-headers="all" data-tx-cols="4">
  <thead>
    <tr><th>Column</th><th>Data Type</th><th>Default</th><th>Nullable</th><th>Description</th></tr>
  </thead>
  <tbody>
    <tr>
      <td><code>user_id</code></td>
      <td><code>uuid</code></td>
      <td>—</td>
      <td>YES</td>
      <td>Owner of the key; references users table.</td>  <!-- Only this cell gets translated -->
    </tr>
  </tbody>
</table>

Result in Italian: columns 0–3 remain byte-identical English. Column 4 becomes "Proprietario della chiave; fa riferimento alla tabella users."

Processing Pipeline

Type D tables are handled in three stages that bookend the translation process:

  1. Phase 0 Extraction — Before any other preprocessing, all tables with data-tx-headers or data-tx-cols attributes are extracted whole and replaced with ⟦TABLE_NNN⟧ placeholders. The model never sees table HTML.
  2. Translation — The document is translated normally (prose mode or standard path). Table placeholders pass through untouched.
  3. Postprocess + Micro-Translate — After translation, placeholders are restored to their original HTML. Then a targeted Bedrock call translates only the declared columns:
    • Extract text from headers (per data-tx-headers) and body cells (per data-tx-cols)
    • Skip cells with translate="no"
    • Batch all strings into a single Bedrock text-mode call using ⟪SPLIT⟫ delimiters
    • Parse response and patch translations back into the table HTML

Cost efficiency: A 185 KB document with 48 Type D tables (Aurora Data Dictionary) costs ~$0.003 for the micro-translate call — all unique description strings batched into one Bedrock request. The full document translation costs ~$0.15. The micro-translate adds negligible overhead.

Common Patterns

Document TypeTypical MarkupEffect
Data Dictionarydata-tx-cols="4"Translate Description column only; Column, Type, Default, Nullable stay English
API Referencedata-tx-cols="0"Translate Action/Description column; Endpoint and Params stay English
Configuration tablesdata-tx-cols=""Translate headers only; all body content (keys, values, examples) stays English
Troubleshootingdata-tx-cols="0,1,2"Translate all prose columns (Symptom, Cause, Fix)

Integration with Prose Mode

Documents with extreme density hints (hint:density=extreme) route through prose mode, which extracts and translates only text fragments from the HTML. Without Type D awareness, prose mode would indiscriminately translate all visible table text — including technical columns that must stay English.

The fix: Phase 0 table extraction runs before prose mode walks the DOM. Prose mode sees only the ⟦TABLE_NNN⟧ placeholder — never the table content. After prose mode rebuilds the document, postprocessing restores the tables and runs micro-translate on the declared columns.

This applies to both the marker-based split path and the auto-split path in handler.py.

Failure Handling

Type D micro-translation is non-fatal. If the Bedrock batch call fails or returns a mismatched fragment count, the tables are restored in their original English — no data corruption, no job failure. The document is delivered with English tables rather than corrupted ones.

16. Notification System

The translation engine sends email notifications via the AutoOps notification pipeline (tbi-ops-notify Lambda → SES). Notifications are consolidated across batch jobs and include detailed per-document breakdowns.

16.1 Email Format

Each notification email includes:

Example Notification

Subject: [INFO] Translation Complete: 2 docs × 11 langs — 22/22 pairs SUCCEEDED

Batch Summary:
• Jobs: 2
• Documents: 2
• Languages: 11
• Total Pairs: 22
• Succeeded: 22
• Failed: 0
• Final State: SUCCEEDED
• Total Time: 7m 12s

Documents Translated:
• Trinity-Beast-TBI-Translation-Engine.html
  ✓ Succeeded: es, pt, fr, de, ru, hi, ja, zh, ar, ur, it
• Trinity-Beast-Infrastructure-Overview.html
  ✓ Succeeded: es, pt, fr, de, ru, hi, ja, zh, ar, ur, it

Deployment:
• CloudFront Invalidations: 2
• All translated files deployed to S3

Search Index:
• Rebuilt successfully (all 11 languages)

Partial Success Example

Subject: [WARNING] Translation Complete: 1 doc × 11 langs — 10/11 pairs PARTIAL

Documents Translated:
• Complex-Technical-Guide.html
  ✓ Succeeded: es, pt, fr, de, ru, hi, ja, zh, ar, it
  ✗ Failed: ur

Error Details:
• Complex-Technical-Guide.html → ur: chunk 14 failed validation after 3 retries
  check_tag_counts: expected 27 code tags, found 23

16.2 Batch Consolidation

When multiple translation jobs are submitted together (e.g., translating 5 documents), the notification system consolidates them into a single email:

This prevents notification spam when translating multiple documents — you get one comprehensive email covering the entire batch, not 5 separate emails.

16.3 Document Resolver (v2.5)

When the same document appears in multiple jobs within a batch (e.g., initial run fails Urdu, retry succeeds), the notification resolves duplicate entries into a single final-state view:

Without the resolver, a retry job would show the same document twice — once with the failure and once with the fix — making the notification confusing and the counts misleading.

16.4 Tag Inventory (v2.8)

Every notification includes a Tag Inventory section showing source vs output tag counts per document. This lets you detect at a glance if the model is adding or dropping tags. As of v2.8, the inventory also reports Mermaid diagram counts:

Tag Inventory (source → output):
• Trinity-Beast-Translation-Service.html
  IN:  code:22 pre:5 strong:8 em:2 a:4 br:3 diagrams:1
  OUT: code:22 pre:5 strong:8 em:2 a:4 br:3 diagrams:1

If the model has a bad day and adds a <span> that wasn't in the source, or drops code tags, you'll see the mismatch immediately:

  IN:  code:23 pre:5 strong:8 diagrams:2
  OUT: code:20 pre:4 strong:8 diagrams:1    ← 3 code dropped, 1 diagram lost

Tag counts are logged per-language in Aurora (translation_job_events) with tags_in and tags_out fields. The notification shows the first successful language's counts (source tags are identical across all languages since it's the same source document).

Recipient: All translation notifications go to CoryDeanKalani@CPMP-Site.org via the unified AutoOps notification pipeline. The sender is CPMP Mission <No-Reply@CPMP-Site.org>.

17. Delta Translation (Incremental Updates)

Documents change frequently — a new endpoint, a revised architecture, an updated pricing table. Without delta translation, every edit requires re-translating the entire document across all 11 languages. Delta translation solves this by identifying exactly which sections changed and translating only those, reusing cached translations for everything else.

17.1 Concept

The delta translation system leverages two key properties of the document library:

By comparing the current English document against the version that was last translated, the system identifies which sections changed (by content hash) and only sends those to Bedrock. Unchanged sections are pulled directly from the existing translated document. Typical savings: 70–90% on incremental updates.

17.2 S3 Versioning as Diff Source

The website bucket (trinity-beast-website-east2) has versioning enabled. Every aws s3 cp or s3api put-object creates a new version with a unique VersionId. The delta system uses this to:

No separate manifest storage is required — S3 already has the full history. A lightweight metadata file (docs/delta/{doc}.{lang}.json) tracks which VersionId was last translated for each document-language pair.

17.3 Comment Preservation (Sentinel Pass 0)

For delta translation to work, <!-- TBI-CHUNK --> markers must survive the translation round-trip. Previously, Bedrock silently dropped HTML comments during translation. The sentinel system now includes a Pass 0 that protects all HTML comments:

# Pass 0: Before Bedrock sees the chunk
<!-- TBI-CHUNK -->  →  __TBP0__    (sentinel token)
<!-- Section 5 -->  →  __TBP1__    (sentinel token)

# After translation: sentinels restored
__TBP0__  →  <!-- TBI-CHUNK -->
__TBP1__  →  <!-- Section 5 -->

This is implemented as the first pass in _apply_sentinels() in engine.py, before the existing translate="no" element extraction (Pass 1), paired span sentinels (Pass 2), and numeric protection (Pass 3). Comments are treated as Type A (FULL) sentinels — extracted completely and restored verbatim.

17.4 Hash-Based Section Matching

The algorithm is position-independent — sections are matched by content hash, not by index. This means markers can be added, removed, or repositioned between versions without breaking the delta logic.

Diagram 17.1: Delta Translation Flow

flowchart TD
    A[Fetch Current English from S3] --> B[Split by TBI-CHUNK markers]
    B --> C[Hash each section SHA-256]
    D[Fetch Previous English version] --> E[Split by TBI-CHUNK markers]
    E --> F[Hash each section]
    C --> G{Compare hashes}
    F --> G
    G -->|Match found| H[Pull from existing translation]
    G -->|No match| I[Send to Bedrock]
    H --> J[Reassemble with TBI-CHUNK markers]
    I --> J
    J --> K[Deploy to S3 + Save metadata]

    style A fill:#1e3a5f,stroke:#60a5fa,color:#e0e0e0
    style D fill:#1e3a5f,stroke:#60a5fa,color:#e0e0e0
    style H fill:#064e3b,stroke:#10b981,color:#e0e0e0
    style I fill:#7c2d12,stroke:#f97316,color:#e0e0e0
    style K fill:#1e3a5f,stroke:#60a5fa,color:#e0e0e0
        

Marker repositioning example:

17.5 CLI Commands

Four KCC commands support delta translation and chunk management:

Delta Diff (Analysis Only)

# List available S3 versions
bash scripts/kcc.sh delta-diff Trinity-Beast-API-Reference.html --list-versions

# Compare current vs previous version (auto-detects)
bash scripts/kcc.sh delta-diff Trinity-Beast-API-Reference.html

# Compare against a specific version
bash scripts/kcc.sh delta-diff Trinity-Beast-API-Reference.html --version-id ksYxUBZIUB8Roi2KQYje6ig9R7JesL9z

# Show delta for a specific language
bash scripts/kcc.sh delta-diff Trinity-Beast-API-Reference.html --lang ja

Delta Translate (Incremental Translation — Local CLI)

# Dry run — show what would change without calling Bedrock
bash scripts/kcc.sh delta-translate Trinity-Beast-API-Reference.html es --dry-run

# Translate only changed sections for one language
bash scripts/kcc.sh delta-translate Trinity-Beast-API-Reference.html es

# Translate changed sections for all languages
bash scripts/kcc.sh delta-translate Trinity-Beast-API-Reference.html all

# Force full translation (creates fresh baseline)
bash scripts/kcc.sh delta-translate Trinity-Beast-API-Reference.html all --force

Delta via Remote API (options.delta)

The delta option is also available on POST /admin/translate — the worker skips any language pair where the translated file on S3 is already newer than the source document. No local CLI needed.

# Submit a delta job via the remote API — skips up-to-date pairs automatically
curl -s -X POST -H "X-Admin-Key: $ADMIN_KEY" -H "Content-Type: application/json" \
  -d '{"docs":["Trinity-Beast-API-Reference.html"],"langs":"all","options":{"delta":true}}' \
  https://api.cpmp-site.org/admin/translate | jq .

Delta Validate (Marker Preservation Check)

# Validate TBI-CHUNK markers survived translation for all delta-enabled docs
bash scripts/kcc.sh delta-validate all all

# Validate a specific doc across all languages
bash scripts/kcc.sh delta-validate Trinity-Beast-API-Reference.html all

# Validate a specific doc + language pair
bash scripts/kcc.sh delta-validate Trinity-Beast-API-Reference.html es

Reports pass/fail per doc×lang pair. Exit code 0 if all pass, 1 if any markers were lost. Run after any translation job to confirm Sentinel Pass 0 is working correctly.

Chunk Sizer (Auto-Placement Suggestions)

# Analyze a doc from S3 and suggest TBI-CHUNK marker placement
bash scripts/kcc.sh chunk-size Trinity-Beast-API-Reference.html

# Analyze a local file
bash scripts/kcc.sh chunk-size /path/to/local/doc.html

Scans the document for <section>, <h2>, <h3>, and .category-section boundaries. Reports current chunk sizes (if markers exist), identifies policy violations, and suggests where to insert markers to stay within the 15KB/18KB/12KB policy. Dense sections (high translate="no" density) automatically target the tighter 12KB limit.

17.6 Bootstrap Path

Existing translated documents do not contain <!-- TBI-CHUNK --> markers (they were stripped before the sentinel fix). The bootstrap sequence is:

  1. First run (full cost): Use --force to translate the entire document. The sentinel fix preserves markers in the output. Delta metadata is saved to S3.
  2. Subsequent runs (delta savings): The tool detects the existing translation has markers, loads metadata to identify the previous English version, and only translates changed sections.

After the bootstrap run, typical savings on incremental updates:

Change TypeTypical SavingsExample
Single section edit85–95%Fix a typo, update one endpoint
New section added70–85%Add a new feature section
Marker repositioned60–75%Split a large section in two
Major rewrite20–40%Restructure half the document

Cost model: At approximately $1.50 per section-language pair, a 9-section document across 11 languages costs ~$148.50 for a full translation. With delta (2 sections changed), the same update costs ~$33 — a 78% reduction.

18. API Error Message Translation

The document translation engine handles HTML docs (Sections 1–17). A separate system handles the translation of API response messages — the short error and status strings returned in UME envelope responses. This section documents how template variables are protected during that process and why the delimiter choice matters.

18.1 Overview

The error_messages Aurora table holds 21 message keys in 12 languages (252 rows total). These messages are served dynamically based on the caller's api_lang preference. Some messages contain template variables that are substituted at runtime with real values (e.g., usage counts, rate limits).

Example message (English):

Monthly query limit reached. You have used [usage] of [limit] allowed queries this month.

At runtime, the Go server substitutes [usage] and [limit] with the user's actual values before returning the response.

18.2 Template Variables — Brackets Over Braces

Convention: All template variables in translatable messages use [square brackets], never {curly braces}.

Why: Curly braces are universally recognized by AI models as structured data delimiters (JSON, format strings, template literals). When a translation model encounters {usage} in a message, it frequently attempts to:

Square brackets do not trigger this behavior. Models treat [usage] as inline notation — similar to citation markers or editorial annotations — and reliably pass it through unchanged across all 11 target languages.

Implementation:

LayerFileBehavior
Go server (substitution)internal/handlers/core_errmsg.gostrings.ReplaceAll(msg, "["+k+"]", v)
Go server (caller)internal/handlers/price.goPasses map[string]string{"usage": "...", "limit": "..."}
Aurora (source)error_messages tableAll messages use [variable] syntax
Translation toolscripts/kcc_helpers/translate_messages.pyWraps [variable] in sentinels before Bedrock translation

18.3 Translation Workflow

API error messages are translated using the translate-messages KCC command — a lightweight Bedrock pipeline separate from the document translation engine:

  1. Source: English messages from Aurora error_messages table (21 keys)
  2. Sentinel wrap: Template variables ([usage], [limit]) and brand terms are wrapped in sentinel tokens (⟦T1⟧...⟦Tn⟧) before the model sees them
  3. Translation: Bedrock translates the prose around the sentinels. The standing model is Qwen 3 235B, and it is selectable per language via translation_language_profiles.model_override — so do not read a specific model name here as fixed. This line said "Claude Sonnet 4.6" until 2026-08-30, months after Qwen became the standing choice.
  4. Restoration: Sentinels are replaced back with the original [variable] text and brand terms
  5. Storage: Translated messages are written back to Aurora (one row per language per key)
  6. Cache: LPO server loads messages from Valkey (errmsg:{lang}:{key}) on first access

The entire process is idempotent — running it again overwrites with fresh translations. No S3, no CloudFront, no search index rebuild. Just Aurora + Valkey.

18.4 Sentinel Protection for Variables

The translate_messages.py helper uses the same sentinel wrap/unwrap pattern as the document translation engine, but at a much smaller scale (single sentences vs. multi-KB chunks):

# Before translation:
"Monthly query limit reached. You have used [usage] of [limit] allowed queries."
→ "Monthly query limit reached. You have used ⟦T1⟧ of ⟦T2⟧ allowed queries."

# After translation (Spanish):
"Límite de consultas mensual alcanzado. Has usado ⟦T1⟧ de ⟦T2⟧ consultas permitidas."
→ "Límite de consultas mensual alcanzado. Has usado [usage] de [limit] consultas permitidas."

Protected items include:

Result: 21 messages × 11 languages = 231 translations completed with zero failures. Template variables survive intact in every language — the Go server can substitute real values at runtime regardless of the response language.

19. Translation ETA System (v5.2)

The ETA system predicts how long a translation job will take, based on historical per-pair duration data from Aurora. It provides time estimates at three touchpoints: job submission, status polling, and pre-submission planning.

19.1 Historical Calibration

The system maintains per-language speed metrics derived from the last 90 successful translation pairs per language (990 samples total across 11 languages). Calibration data lives in Valkey (tx:eta:calibration hash, 6-hour TTL) and is rebuilt automatically on every job completion.

Calibration Metrics (Per Language)

MetricDescription
median_secs_per_chunkMedian seconds per chunk — the headline speed factor (always a multiple of 3)
p25_secs_per_chunk25th percentile — used for the optimistic (low) estimate
p75_secs_per_chunk75th percentile — used for the pessimistic (high) estimate
sample_countNumber of historical pairs this language's calibration is based on (max 90)

Observed Speed Factors (June 2026)

LanguageMedian s/chunkRelative Speed
Arabic18Fastest (compact script)
Chinese, Russian, Italian, Urdu21Standard
Spanish, Portuguese, French, German, Hindi, Japanese24Slightly slower (expansion/complexity)

All median values are rounded to multiples of 3 — The Trinity Beast convention.

19.2 ETA Formula

For a job with N documents and M languages (running in parallel, one container per language):

per_language_seconds = startup_overhead + (total_chunks × lang_speed_factor)
job_eta = max(per_language_seconds for each language)
range_low = max(startup + total_chunks × p25 for each language)
range_high = max(startup + total_chunks × p75 for each language)

Languages run in parallel (one container each). The wall-clock time equals the slowest language (the bottleneck). The bottleneck language is identified and reported in every ETA response.

19.3 API Endpoints

EndpointMethodPurpose
GET /admin/translate/eta?docs=X&langs=YGETPre-submission estimate — plan before committing
POST /admin/translate/eta/calibratePOSTForce rebuild calibration from Aurora (on-demand)
POST /admin/translate (response)—Immediate ETA in the submission response
GET /admin/translate/status/{job_id}—Live ETA with elapsed/remaining/percent for running jobs
POST /translate/quote (response)—Customer-facing delivery time estimate (calibration-based)

19.4 Live Progress Tracking

For running jobs, the status endpoint returns an eta object with real-time progress:

{
  "estimated_seconds": 202,
  "estimated_low_seconds": 153,
  "estimated_high_seconds": 246,
  "estimated_formatted": "3m 22s",
  "estimated_range": "2m 33s – 4m 6s",
  "bottleneck_lang": "hi",
  "elapsed_seconds": 118,
  "remaining_seconds": 84,
  "remaining_formatted": "1m 24s",
  "percent_complete": 58
}

Progress is calculated as elapsed / estimated × 100, capped at 99% until the job actually completes. The remaining time decreases linearly — no smoothing or prediction adjustment mid-flight.

19.5 Auto-Calibration

Calibration rebuilds automatically in a background goroutine on every job completion (RebuildETACalibrationBackground() called from TranslateCompleteHandler). This ensures the speed factors reflect the most recent translation performance — model updates, infrastructure changes, or document complexity shifts are absorbed into the next prediction within one job cycle.

Design principle: Predict from reality, not theory. The ETA system uses zero configuration or manual tuning — it calibrates entirely from historical job data. Every completed job improves the next prediction. The ±range (p25/p75) naturally captures the variance window without requiring an explicit uncertainty model.

20. Integrity Invariants (v5.9)

Everything above describes what the engine does. This section describes what it is not allowed to do — thirty invariants, each one earned by an incident, each one enforced at a named point in the code and pinned by a test that fails if the enforcement moves.

They are documented here rather than left in the codebase because a translation pipeline fails in a particular way: quietly, and in the artifact rather than the process. A job reports succeeded, the email says every pair completed, the cost is right, and the published document is wrong. Every invariant below exists because that happened at least once.

20.1 Why This Section Exists

Three properties of this system make ordinary testing insufficient, and understanding them is the difference between reading the invariant list as bureaucracy and reading it as hard-won architecture.

The model is stochastic, so the same input can fail once and pass on retry. A defect that appears in one language and not the other ten is not evidence of a language-specific bug — it is the normal shape of model variance. That makes reproduction expensive and makes deterministic guards more valuable than diagnostic ones. Wherever a guard can be made exact, it is worth far more than a guard that is merely usually right.

The pipeline repairs as well as validates, and repair can create defects. There are more than twenty repair, recovery and restore functions between the model's output and the published bytes. Roughly half put back exact source bytes, which is always safe. The other half guess where a wrapper belongs, and a wrong guess is indistinguishable from correct output to anything downstream. The standing direction is to preserve deterministic restoration and prefer hard failure with full diagnostics over heuristic repair.

A check is only as good as the string it reads. This is the subtlest of the three and it produced Invariant 12. A gate can be perfectly correct, pass cleanly, and validate nothing — because the artifact it examined is transformed afterwards. Position in the pipeline is part of a check's correctness, not an implementation detail.

The governing lesson, stated once: a tool that answers is not a tool that is right. Every invariant below is accompanied by a test whose job is to prove the check can actually fail. A green run on an assertion incapable of failing is worse than no assertion, because it carries the authority of a passing test.

20.2 The Thirty Invariants

Each invariant states an obligation, names where it is enforced, and names the test that would catch its removal. The full incident narrative for each lives in INVARIANTS.md alongside the worker source.

Invariant 1 — Chunking must never change the document's text content

Splitting a document into chunks and reassembling it must be a byte-level identity operation on the text. The mechanism that broke this was an unconditional strip of the model's output, which discarded the boundary whitespace a chunk carried at its edges. Two chunks then joined with no space between them, fusing the last word of one to the first word of the next.

Enforced by capturing each chunk's leading and trailing whitespace from the source before translation and reattaching it afterwards, regardless of what the model returned. Pinned by a fuzz harness that walks the full offset space of a chunk boundary with several simulated model behaviours, plus a control proving it detects a deliberately planted defect.

Invariant 2 — A protected term must never end up glued to adjacent text

A brand term that survives translation but arrives fused to the neighbouring word — Valkey followed immediately by a translated word with no space — is preserved by every presence check and broken to every reader. Presence and correct placement are different properties, and a substring check answers only the first.

Enforced by a spacing check that compares the full glued run at each occurrence against the glued shapes the source already had, per occurrence rather than per term. The per-occurrence rule matters: the same short term can be a legitimate compound identifier in one place and ordinary prose in another within the same chunk, and a per-term check lets one occurrence's shape license a false verdict for the other.

Calibrated for Arabic, August 2026. The check extends a term's glued run past any adjacent alphanumeric character, and Arabic letters are alphanumeric — so the conjunction wa-, which Arabic orthography always writes bound to the following word, read as a lost space and hard-failed the chunk. Arabic was consequently the only language unable to publish this very document. The exemption is deliberately narrow: exactly one proclitic conjunction, nothing bound on the right, and Arabic only. Urdu uses the same letter but its conjunction is a free-standing word, so the exemption does not extend there.

Why narrow. A blanket "ignore Arabic letters to the left of a term" would have cleared the false positive and hidden every genuine lost space in Arabic and Urdu. Measured across the published corpus, the narrow rule cleared 38 false findings and left the genuine ones — Urdu words fused directly to Latin terms — still flagged. That ratio is the evidence the exemption is correctly sized.

Invariant 3 — A strip on model output must not destroy content a later step needs

Invariant 1's root cause generalised. Every call that trims whitespace from raw model output or extracted text was inventoried and classified. Two share the dangerous shape and are safe only because of a call-order dependency rather than by design — which is precisely the kind of safety that breaks silently when someone reorders code.

Enforced by documentation plus tests that pin the call order those two depend on, so a future reordering trips a test rather than shipping a defect. This one is honest about its own limits: it is a snapshot of the call sites that existed when it was written, not a check that re-derives the inventory.

Invariant 4 — A chunk marker must never sit at a structurally unsafe position

A marker placed inside an open element splits the document mid-structure, and the resulting fragments are not independently valid HTML. Several tools place markers automatically, so the constraint has to live where the placement happens rather than in a reviewer's judgement.

Enforced by moving a proposed marker to the nearest safe boundary rather than rejecting it, and by a validator that re-implements the server's exact line-recognition rule. The marker rule is stricter than it looks: a marker must sit alone on its own line. A marker glued to an adjacent element passes a naive search and is invisible to the splitter, so the intended boundary silently disappears into the previous chunk.

This document is the hardest case for its own tooling. Because it documents the marker syntax, it contains marker mentions in prose alongside real markers. A substring count over-reports by roughly 40 percent, and a chunk-size calculation keyed to that loose pattern splits real chunks into fragments and therefore understates their size — a false all-clear on the size policy. Any count or size check on this document must apply the marker rule, not a substring match.

Invariant 5 — The corpus's actual state must be measured, not assumed

A job's reported status is a claim about the job, not about the document. The published artifact is the only authority on whether the published artifact is correct, and it lives on the CDN rather than on any developer's disk.

Enforced by audits that read the live CDN and enumerate from the English document set rather than from a language folder — a distinction that matters because the engine writes translations straight to S3, so a local language folder may not even contain the file that is live. Judged on absolute counts rather than ratios, because a share-of-content ratio flags legitimate short samples and produces noise.

Invariant 6 — No extracted placeholder may be permanently lost

Every extracted element must come back, regardless of which extraction mechanism produced it or why the model failed to echo its token. The incident was an entire thirty-row table dropped whole, because its token sat alone at the start of a chunk with almost no surrounding prose to anchor it.

Enforced by a three-tier recovery in the one function all extracted types funnel through: exact match, then fuzzy match for a garbled token, then anchor recovery for a token dropped outright with no trace to match against. Nothing upstream can guarantee a model never drops an isolated token, so recovery belongs at the point where every type converges rather than in a hope about model behaviour.

Invariant 7 — A chunk's output must never collapse into a repeated character

A model that loses its place can fill its entire output budget with one character. Two published Urdu documents each served a solid wall of a single glyph where prose should have been — over seven thousand repetitions in each — and both were reported as successful translations.

Enforced by a generation-time guard that rejects a chunk whose output degenerates into a repeated character or a short cycle, detected on run length rather than script. The script-based instinct is wrong here and the measurement proves it: one live case was thousands of Arabic diacritic marks inside Urdu's own script, which a cross-script check structurally cannot see.

Root cause, corrected August 2026: placeholder density per chunk, not chunk size. Both collapsing chunks were comfortably inside the size policy — well under half the ceiling. What they shared was a dense concentration of placeholders relative to translatable prose, which leaves the model too little context between opaque tokens. Isolating the dense region behind its own chunk marker fixed both documents, and the language then succeeded three consecutive times. Chunk size is the wrong lever for this failure.

Invariant 8 — A placeholder-shaped literal in the document's own content must never be replaced

Restoration replaces a placeholder token with the element it stands for. Any text that merely looks like a token is therefore indistinguishable from a real one and gets overwritten with unrelated lifted HTML.

This document is the reason the invariant exists. It documents the placeholder format and so contains roughly thirty placeholder-shaped literals. One of them was replaced by a real element, silently, with no model involved — reproduced with the model removed from the loop entirely, which is what established the defect as ours rather than the model's.

Enforced by scanning the source for placeholder-shaped literals before generating any token and starting the index above every literal found. Because a single counter is shared across all placeholder types, token equality requires index equality, so clearing the highest reserved index makes a collision arithmetically impossible.

Bounding the replacement to one occurrence was considered and rejected. "The first occurrence" is not reliably the real token, so bounding trades a visible corruption for an ordering-dependent one. That rejection is load-bearing for Invariant 11 below — read them together.

Invariant 9 — HTML parsing must never alter the case of an SVG attribute name

HTML attribute names are case-insensitive; SVG attribute names are not. A general-purpose HTML parser lower-cases attribute names, which turns an inline diagram's coordinate-system declaration into an attribute browsers ignore. The graphic then loses its ability to scale — visibly broken, and invisible to every tag-count check, because the tag is still there and the attribute is still there.

Enforced by extracting inline vector graphics as raw bytes before the parser can normalise them.

Invariant 10 — Scaffolding removal must remove only scaffolding, from the right elements

Preprocessing enriches the source before translation — wrapping brand terms, marking code elements as non-translatable. Removing that scaffolding afterwards must be bounded by the excess over the authored document, and it must be targeted by content rather than by count.

The defect was an unconditional removal sitting a few lines below a branch that got the same problem right. Because the house convention requires every authored code element to carry the non-translate marker, "remove them all" is precisely backwards for this corpus — almost none of them are scaffolding. On this document the source carried 628 such attributes, preprocessing added 74, and all 702 were destroyed. Every published translation of every document shipped none of them.

Counting is not enough, and this is the instructive part. A count-limited fix preserves the right number of attributes and leaves them on the wrong elements, because a plain replacement takes the first matches and those are the authored ones. It matters beyond tidiness: these pages carry a browser translation widget, so a code element that loses its marker gets its copy-pasteable command translated for the reader. The correct fix records which contents were bare in the authored document and removes the attribute only from those.

Verified by a round trip that is byte-identical to the authored source across all 471 distinct code contents in this document.

Invariant 11 — One extracted element must produce exactly one copy

Restoration must produce a single copy of an element however many times the model echoed its token. The defect was a boolean presence check followed by an unbounded replacement: a boolean cannot distinguish one occurrence from five, and an unbounded replacement expands every one of them into a full copy of the lifted element.

Model doubling is not a hypothesis here. The sentinel system that preceded the current preprocessor carried an explicit pass to collapse duplicated tokens, commented as a bilingual output instinct. That defence was not carried over when content protection moved to the preprocessor, which makes the duplication a regression introduced by that migration rather than a new model behaviour. The current handling deliberately does not inherit the old pass's limitation of only collapsing adjacent duplicates.

Enforced by counting occurrences instead of testing presence: none means missing and still flows to Invariant 6's recovery untouched, one is the normal path, and more than one restores a single copy at the first position, removes the surplus tokens, reports the placeholder and the count, and fails validation.

This depends on Invariant 8 and is unsafe without it. Invariant 8 rejected bounded replacement because a generated token could collide with the document's own literal — and its own fix removed that possibility by reserving the index space. Bounding is sound because Invariant 8 holds. It is a dependency, not an independent fix, and if the reservation guard is ever weakened this becomes silently unsafe again.

Known boundary: this guarantees the element is not duplicated. It does not repair duplicated prose around a doubled token — in a bilingual-doubling event the model may emit a whole passage twice, and restoration cannot adjudicate which copy to keep. The warning says so, and the corpus audits are where that surfaces.

Invariant 12 — Something must validate the bytes that actually ship

A gate that validates a string the pipeline then transforms has validated nothing. At least one check must observe the artifact as it will be written.

An audit of gate positions found that none of the full-document gates observed the published bytes. All of them ran at one point, and the output was then reassigned seven more times before the write with nothing validating any of it. Two of those reassignments are content transformations acting on wrappers and non-translate markers — exactly the dimension the earlier gates check. So those gates compared two strings, neither of which shipped, and the shipped artifact was then altered in the very dimension they had just validated.

The obvious fix is the wrong one, and it was measured before being attempted. Moving the gates later false-fails. Both sides of the existing comparison carry scaffolding, so its baseline is correct for the position it occupies. Moving the gates and changing their baseline are only safe together; either one alone produces a spurious 13-element mismatch on this document. An earlier draft of the audit called that baseline a defect — that framing was wrong, and acting on it would have introduced the false failure it was trying to prevent.

Enforced by a differential check immediately before the write, which asks only what the final transformations did. That sequence is deterministic code, so its contract is exact — and an exact check is safe there precisely because it cannot be tripped by the stochastic model drift the earlier tolerance-based gate exists to absorb. Four contracts: scaffolding removal may only reduce non-translate markers and never below the authored count; wrapper repair may only add wrappers; path and language rewrites may not change any tag count; and no structural tag may lose or gain count across the sequence.

It fails hard rather than downgrading. The earlier gate's downgrade path exists so stochastic model drift does not discard a paid-for translation — but a finding here cannot come from the model, so it means the engine's own deterministic code misbehaved.

Invariant 13 — No regular expressions anywhere in the engine

Every string operation in the engine uses direct comparison: membership, equality, find(), character walking, or a real DOM parser. Regular expressions are excluded from the engine entirely — not discouraged, not restricted to non-validation use. Excluded, with the same absolute standing that forbidding omitempty has in the platform's response envelope: the construct does not appear in this codebase, and a reviewer who finds one has found a defect rather than a style disagreement.

This is the only invariant on the list that constrains the source rather than the pipeline, and it is here because every other one kept tracing back to it. A pattern matches characters. Every job in this engine — is this term preserved, is this element intact, did this wrapper survive, is this boundary correct — is a question about meaning. Where those two diverge the pattern still returns an answer, and the answer is silently wrong.

Three failures paid for this rule, each one silent:

Enforced by a repository audit that walks every engine source file and reports any import of the pattern library or any call into it. It runs as a standing leg of the daily audit sweep and returns a non-zero exit on any finding, so a reintroduction fails the sweep rather than waiting to be noticed. It was verified in both directions before being trusted — clean across the engine, red on a deliberately induced violation, clean again after reverting it.

The audit reads the parsed syntax tree, not the file's text, and that distinction is the whole reason it works. This codebase documents its direct-comparison functions by naming the pattern each one replaced, so the source is full of accurate prose about patterns. A text search reports every one of those sentences as a violation of the rule they exist to explain. The first inventory attempt was a line-count search: it reported roughly fifty-seven usages across eleven files and was quoted as a measurement. The true count of executable usages was four, in a single file. A language's own parser cannot be misled by a comment, and no count taken any other way should be trusted.

One declared exception. A test may reinstate a retired pattern as a differential oracle — running the old pattern and the new direct-comparison implementation across the entire corpus and asserting identical output. That is how a conversion earns confidence instead of receiving it, and the conversion that closed out this invariant was proven that way across every English source in the library: identical output, four hundred and twenty wrappers rewritten, zero disagreements. A file claiming the exception must declare it with an explicit marker so the exemption is auditable rather than assumed, and the oracle should be retired once the property it proved can be stated directly.

Invariant 14 — A translated fragment must land in the slot it was translated for

Prose-mode translation sends the model several fragments in one call and gets back a labelled list — fragment 3's translation goes in slot 3, and nothing beyond the model's own label ties the two together. A model that merges two fragments while continuing to count shifts every later fragment one slot into its neighbour's place. The result is well-formed, correctly scripted, and tag-count-identical to the source, because nothing about a shift changes any of the properties those checks measure. It shipped an Arabic section heading reading "the duplicate" and a key card holding the back half of an unrelated sentence, with the job reporting succeeded.

Two deterministic rules, neither needing a model call. Displacement: a unique anchor from fragment N found in a different slot proves the content moved — stated as presence in the wrong place rather than absence from the right one, because absence alone is ambiguous (the model may simply have mangled the identifier) while presence elsewhere is proof. Deletion: a slot left empty while its source fragment was not is content silently dropped, with the writer producing an unremarkable whitespace-only text node.

Coverage was measured against the real corpus rather than assumed, and the number is worth sitting with. Only 2 percent of fragments across the library carry a unique anchor at all, so rule one is individually blind to the other 98 percent. What rescues it is that a shift propagates forward: 82 percent of the positions where a shift could begin have an anchored fragment somewhere downstream of them within the same chunk, which is enough to catch the shift even though the specific fragment that started it carries no anchor of its own. That 82 percent is a per-document ceiling, not the gate's guaranteed rate — three documents in the library carry no anchors whatsoever and sit entirely outside rule one's reach. Rule two needs no anchor and covers deletion completely.

Enforced by a check run before the DOM is touched, on the model's raw labelled response. The engine's own retry path takes one corrective pass at a higher temperature before treating a persistent finding as a hard failure; a caller-side recovery fallback that keeps the original document when a fix cannot be made declines rather than guesses, honouring its own contract instead of shipping a repair it cannot verify.

Known boundary: a swap between two anchor-less fragments sharing a parent is undetectable by this mechanism — it needs semantic comparison, which means another model call, and that tradeoff was declined rather than hidden. The gap is named rather than closed, and the corpus audits are the backstop that can still catch it after publication.

Invariant 15 — Two individually correct restorations must not touch with no separator

Invariant 11 guarantees one placeholder produces exactly one copy of its element. It says nothing about what happens when two different, individually healthy placeholders happen to land next to each other during restoration. The defect this closes is two distinct product names arriving glued with zero space between them — the same brand term doubled with no separator, from two entirely separate sentences — where neither placeholder was missing, neither was duplicated, and every existing check that looks at one restoration at a time reports it clean.

The model dropped every character of connecting prose between two adjacent-in-output placeholders. The main restoration loop asks "is this token okay" for each entry in isolation and never asks what now sits on either side of a splice it just made. A join is exactly the place that question has to be asked, because a join is the only point where two independently-correct pieces first touch.

Enforced by a guard applied at every splice point in restoration, reused rather than duplicated from the join logic anchor recovery already needed for the same reason. It inserts a single space only when concatenating the two sides bare would present two touching alphanumeric characters to a reader — the same visible-character test, deliberately, that the tail's brand-term glue repair uses, rather than a second independent definition of "glued" that could drift from the first. Japanese and Chinese are exempted, because those scripts legitimately attach a foreign term to adjacent text with no space, and inserting one there would itself be the defect.

Surfaced as a parity-audit regression — three known defects becoming seven overnight — rather than found by reading code, which is the same discipline as Invariant 5: the corpus's actual state is what proved something had changed, not a hunch about what might have.

Invariant 16 — A response cut off at the model's token ceiling must never ship as a completed translation

Every call into Bedrock returns a stop signal distinguishing a response that finished from one cut off for exhausting its output budget. Nothing before this invariant read that signal. A truncated response is real, well-formed text right up to the cutoff — it fails no content check by virtue of being incomplete, because incompleteness is not a property content checks can see. It is a property of how much content exists at all, and a document is missing everything past the cutoff while every tag-count and brand-term check reports what little text arrived as clean.

A published document shipped with its footer and every closing tag cut off entirely. The failing chunk hit the exact same output-token count on all four attempts — a ceiling divided evenly by the retry count, meaning it was not occasionally near the limit, it was pinned to it every single time. The raw response confirmed the real cause directly: the model was cut off mid-reasoning, before writing a single character of the actual translation, and the pipeline shipped what little text existed anyway.

The compounding cause, and why raising the ceiling alone would not have been enough. The model's reasoning-token budget had been configured larger than the total completion ceiling. Reasoning tokens are not a separate allowance — they draw from the exact same pool the output ceiling caps. A configuration where reasoning could alone exceed the total budget meant any chunk dense enough to make the model think at length was one step from guaranteed truncation, independent of how much actual translation text would eventually have been needed. Both halves had to be fixed together: the reasoning budget capped well below the completion ceiling, and the ceiling itself raised and tested directly against the account's real limit rather than the model card's advertised one, which the account rejected outright.

Enforced by reading the provider's real stop signal per model family and treating a truncated attempt as a hard failure that retries through the existing loop rather than falling through to a return — exhausting every retry raises rather than shipping the partial result. An unclosed reasoning block is treated as truncation evidence in its own right, since the step that would otherwise close it deliberately leaves an unclosed one untouched rather than guessing where it should end.

Relationship to Invariant 7: Invariant 7 catches a chunk that degenerates — collapses into a repeated character while still consuming its full budget doing so. This invariant catches a chunk that runs out of budget before finishing, producing clean, non-repeating text that simply stops. Different failure shape, same root category: a completion-time content loss no placeholder-based recovery can see, because there was never a placeholder standing in for the missing content. Nothing was ever generated to recover.

Invariant 17 — A token format the prompt does not name will be rewritten into one it does

Every placeholder or sentinel format that can reach the model must be named explicitly in its instructions, and those instructions must forbid converting one format into another or inventing a format absent from the input. A chunk that returns fewer placeholders of a given format than it was sent is a hard failure that retries.

One extraction pass lifts protected content into a placeholder format the rest of the engine treats as opaque — it neither mints that format nor restores it, so nothing verified the tokens came back. Meanwhile the model's instructions named every other placeholder format unconditionally and said nothing about this one. Given tokens it could see and instructions about formats it could not, the model did the plausible thing: it rewrote what it had into what it had been told about, and in doing so satisfied the instructions' own counting rule. From the model's own point of view, it had complied.

Five consecutive identical production failures, and two wrong fixes built against a caption before anyone checked whether the caption was a diagnosis. Every failure reported the same hardcoded finding text naming a specific repair step, because that check's finding string is written once for glue found anywhere across the entire tail — a location hint, not a cause. Two build-and-deploy cycles were spent correcting the step the message named. Both were real, tested fixes to real, if unrelated, defects; neither touched the actual fault, and either one succeeding would have quietly shipped untranslated English into a published document instead of blocking it — which is the failure the block was there to prevent.

What finally solved it was reading the pipeline directly rather than reasoning about it from a symptom three layers downstream. A neighbouring chunk with more placeholders round-tripped perfectly through identical code, which eliminated density, size and structural imbalance as causes in one comparison. The failing chunk sent fifty of its own placeholders and returned zero of them — while returning over fifty tokens in the format the model had been told about, tokens the engine's own minting function had not created. The engine did not produce them. The model did.

Every retry had failed identically because temperature was zero and the input never changed — a deterministic-input fault is precisely the shape a retry cannot fix, the same lesson this engine already carries from the placeholder-residue invariant's own reverted blocking gate.

Enforced by two changes made together, because neither alone was sufficient. The instructions now name the previously-unmentioned format explicitly, forbid converting between formats, and count survival per format rather than globally — closing the cause. And a chunk returning fewer of that format than it was sent is now a hard failure routed into the existing retry loop — inert whenever none were sent, so ordinary chunks are unaffected — catching any recurrence loudly instead of letting it corrupt a document silently. The prompt fix removes the trigger; the gate makes a future instance loud rather than invisible.

Invariant 18 — No engine-minted placeholder residue may appear in the published bytes

Every manifest-driven recovery in this pipeline — Invariant 6's three-tier recovery, the fuzzy match, the anchor recovery — asks one question: did the element I extracted come back? That is the absence side of the question. Nothing asked the presence side: is there placeholder-shaped residue sitting in what is about to ship. A document could restore every manifest entry, pass every gate above, and still publish garbage in its prose, because none of those gates read for a token shape — they read for tag counts and term presence.

The German copy of this very document once shipped four raw sentinels in a single sentence explaining a different subsystem — the same content class this section now documents, published as literal tokens rather than the product names they stood for. A corpus sweep at the time found leaked tokens in over a tenth of all published pairs, across every one of the eleven languages, concentrated in the languages whose scripts already run densest.

Re-translation is not the remedy, and this was proven rather than assumed. Two separate runs of the identical document in the identical language produced different bytes carrying the identical leaked token identifiers. The fault sits in the restore step, not in sampling variance, so spending Bedrock tokens on a fresh translation changes nothing about whether the defect recurs.

Enforced by a check immediately before the write that compares every engine-minted token shape against the English source: anything the source legitimately carries — this document discusses its own sentinel syntax in prose, so it is allowed to contain the shapes it explains — passes, and anything the shipped bytes carry that the source does not is stripped from the bytes and reported as a loud warning rather than blocking the publish. It does not block, deliberately: the prior version did block, and making roughly two dozen documents permanently unpublishable while calling the failure "recoverable" was itself the defect, once the model was shown to fail the exact same way on retry.

A companion audit sweeps the already-published corpus for the same shapes, because this gate only stops new residue from shipping — it does nothing for what published before it existed. Neither replaces the other, the same relationship the degeneration guard has with its own corpus audit.

A note on numbering. This document's own Invariant 13, several sections above, documents the engine-wide exclusion of regular expressions — a real and separately-enforced rule. The worker's own source history assigns the number 13 to the placeholder-residue gate documented here instead. Both invariants are real, both are enforced, and both are worth reading; only the label diverges between this document and the source repository, and it is called out here rather than silently reconciled, because a silently reconciled number is exactly the kind of small, confident correction that erases a real fact for whoever reads the source next.

Invariant 19 — A chunk without its own document wrapper still gets the document's own size discipline

The chunker's job is to split a document into pieces small enough for reliable translation, and that job does not change just because the piece it was handed is itself already a fragment of a larger one. A caller that hands the chunker a bare slice of body content — no surrounding document — must receive the same size-bounded packing a full document would, not a single unsplit pass-through.

The chunker's entry point locates a document by its opening body tag before deciding how to split it. When that tag is absent — which is exactly the shape of every marker-delimited piece an upstream splitter produces, since a slice of body content never carries its own body tag — the function had one branch left: hand the whole fragment back as a single chunk, no packing applied. That branch was written for what its own comment called an unusual case. It was, in fact, the case every marker-based document hits on every one of its pieces, every time, because the upstream splitter's output is nothing but bare fragments.

The consequence sat quietly underneath a language's own configured size ceiling. A fragment several times over that ceiling went to the model as one uninterrupted chunk, because the layer meant to enforce the ceiling never ran on it — it had already exited through the branch that skips packing entirely. The languages with the tightest ceilings, set tightest because their scripts need the most room per character, were exposed to the largest oversized chunks relative to their own limit.

Enforced by routing the no-wrapper case through the identical marker-check-then-packing logic a full document already receives, rather than exiting early into an unpacked pass-through. A fragment that carries markers still splits on them, deterministically, exactly as before. A fragment that does not is now packed against its target language's real size ceiling, the same discipline every full document already had. Verified against the two live fragments that had been silently exceeding their ceiling by more than double: both now split into multiple correctly-sized pieces, and a fragment already under its ceiling is confirmed to remain a single chunk — the fix adds packing where it was missing without over-splitting where it was never needed.

Invariant 20 — A retry after a specific, identified failure must correct that specific failure, not restate a generic warning

A retry exists to give the model a genuine second chance, and a second chance that repeats the same generic caution the first attempt already had is not a second chance — on a deterministic model call, an unchanged instruction produces the same error again. When a retry is triggered by a hard failure whose cause is already known and already named, the correction offered on that retry must name the same thing.

A chunk that failed on a specific placeholder-survival violation — the model overwriting one token format with another — was, on retry, handed only the pipeline's general-purpose caution about protected terms and version numbers. That caution says nothing about token formats. The retry consumed a full attempt and a full Bedrock call while asking the model to be more careful about a problem it did not have, leaving the actual problem completely uncorrected.

Enforced by carrying the specific hard-failure name forward from one attempt to the next, and selecting a directed correction that restates the exact rule violated whenever that name matches a defect with a known, nameable cause. The general caution remains the default for every other failure shape, so a retry answers what actually went wrong rather than a fixed script of caution that may or may not apply.

Invariant 21 — A protective extraction that hides text from the model must also arrange for that text to be translated

Lifting an element whole to keep the model from mishandling its wrapper is a valid defense against one failure — but if the lifted element's own visible text was the reason to translate it in the first place, the defense has traded one failure for another. An extraction that protects a wrapper by hiding everything inside it, including ordinary prose, must be paired with a step that still translates that prose, or the fix silently becomes the new source of untranslated content.

Short bold and emphasis labels were lifted as complete, single-piece placeholders specifically because the model tended to drop the wrapping tag around them. That defense worked exactly as designed — the wrapper always survived. It also meant the model never received the label's own text as something to translate, so restoration correctly put back exactly what had been lifted: the original English, verbatim, every time. A measurement across a published document found the majority of its bold labels — product tier names, short lead-in phrases — still in English, and every affected label was short enough to match the extraction's own threshold precisely, which is what confirmed the extraction itself as the cause rather than a model shortcoming.

Enforced by the same repair already proven for an identical gap in plain-text links: after restoration, the visible text inside a lifted label is extracted a second time, translated in one small batch call, and patched back into that specific element only. An element whose text comes back unchanged — a protected brand term the model correctly declined to translate — is left exactly as restored, and an element whose only content sits behind its own protection boundary is skipped before any call is made.

Invariant 22 — A part of a split document must carry the same unmatched-tag shape as its own source

A document too large for one model call is split into parts, and a part is almost never internally balanced: a container opened in part 0 may legitimately close in part 6. So "does this part balance" is the wrong question. The right one is whether the part's boundary signature — the list of tags whose partner lives in another part — is identical in the source part and the translated part. A whole-document tag count cannot see this: the counts stay equal while the nesting moves. Measured when it broke, 114 unmatched closers and 109 unmatched openers were dropped across 38 published pairs, and one lost closer ended a layout wrapper 14% into a page so the rest of the document rendered outside it. Every existing gate passed, because the div counts matched the English exactly.

Enforced by encoding each unmatched boundary tag into an inert comment sentinel that carries its own original text, so it survives the model call rather than being hidden from the parser and then handed to the model. Pinned by a same-input, same-position comparison of the two signatures, which has no false-positive mode on a correctly translated part.

Invariant 23 — An element whose prose must stay translatable cannot be lifted whole

Protecting an element by replacing it with a single placeholder guarantees its tag survives and guarantees its text is never translated — the model never sees the words. For a short label that trade is acceptable and is repaired afterwards. For a long emphasised passage it is not: the reader gets a paragraph of English inside an otherwise translated page.

Enforced by protecting a long emphasis wrapper as two independent placeholders, one for the opening tag and one for the closing tag, with the prose left visible between them. Corpus exposure for this shape went from 252 affected elements to zero. Pinned by a threshold measured on authored text rather than on the engine's own tokens — the first version of the measurement counted placeholder characters and picked a boundary that protected the wrong things.

Invariant 24 — A recovered element must never be inserted into markup

When the model drops a placeholder outright, the engine recovers the element by locating a surviving neighbour and inserting the stored content next to it. Locating that neighbour by plain string search over the whole restored document will happily return an offset inside an HTML comment, inside an attribute value, or in the middle of a tag. Reproduced offline at no cost: the anchor was the four-character string 12.2, and the insertion landed inside a comment, so three emphasis labels per language vanished from the page while every tag count still matched.

Enforced by a safe-position gate that refuses any offset not in document text. Unsafe relocations across the corpus went from 44 documents to zero. Pinned by a test asserting no recovered element may be placed inside a comment, an attribute, or a tag.

Invariant 25 — An element's position is its position in the document, not its order of extraction

Rejecting a bad insertion point only moves an element to the next safe wrong place. Closing the harmful shape of Invariant 24 left the position defect intact, and the labels simply moved from a comment into the following heading — visible and wrong, where before they were invisible and wrong. Extraction runs in phases, so a dense block can hold a low index while sitting late in the page: measured, extraction order disagreed with document order for 15,319 of 15,909 tokens across 430 of 443 parts.

Enforced by three changes measured separately, which is the part worth remembering. Assigning a true document-order position and sorting recovery by it moved almost nothing on its own — 611 clean to 612 — and it is the obvious fix. What delivered the improvement was recovering in token space, before substitution, when every anchor is still a unique token rather than content that may appear identically elsewhere. The third was a scope bound plus a requirement that both flanking entries survive, because document order says which entries flank a dropped one but nothing about how far away they are: in one measured case a term inside a title at offset 161 had its next neighbour at offset 5,414, past a stylesheet block and outside the document head.

Invariant 26 — No raw placeholder token may reach a reader through the recovery path

The content-anchored recovery pass runs after substitution and inserted its stored markup verbatim. An entry whose stored content held a nested token therefore had that token inserted with no pass left to resolve it, and the page rendered the literal text of an engine placeholder. Invisible to every gate in the pipeline: the tag counts matched, the placeholder genuinely was restored, and a leaked token is ordinary text rather than markup. Measured corpus-wide, 352 of 15,919 recoveries leaked a token; after the fix, none did.

Enforced by resolving nested tokens before insertion, with a bounded depth so a self-reference cannot loop — and by token-space recovery, which avoids the class by construction. Pinned by a test asserting no recovered element leaks a raw token.

Invariant 27 — A guard that refuses a retry must know when the engine itself changed

The engine records a pair's past failures and refuses to spend tokens repeating one. The question it has to answer is "has anything changed that could make this succeed," and that has two possible answers — the source document, or the engine. It consulted only the first. So immediately after an engine fix, the pairs that most needed re-translating were precisely the ones permanently refused, and silently, because a refusal is terminal.

Enforced by stamping the build time into the image and admitting a retry when either the engine or the source is newer than the recorded failure. Confirmed live: of twelve queued pairs, the seven carrying failure history from a month earlier were all admitted without any manual override. Pinned by a deliberate negative result — the stamp is generated at build time and is absent from a fresh checkout, so the guard warns loudly rather than failing open, and the protection is image-only by design.

Invariant 28 — A model-added closing tag may be dropped only when it is provably surplus

Because a long emphasis wrapper reaches the model as two sentinels rather than as tags (Invariant 23), the model sometimes returns both sentinels and a literal closing tag, having inferred that the prose it just translated was emphasised. After restoration there is one closer too many and the boundary-signature gate correctly refuses to publish. The gate was right; what was missing was a repair for the one shape that is safe to fix.

Enforced by a repair that runs before the gate and only ever removes a tag it can prove is redundant. Four conditions must all hold: the part's unmatched openers already match the source; every extra closer is inline emphasis, never a structural container; the opening-tag count for that name is unchanged from the source; and the signature must equal the source's after the deletion, re-measured rather than assumed. The third condition is the proof — eighteen opens and nineteen closes against a source of eighteen and eighteen leaves the nineteenth closer with nothing it could possibly close, whereas seventeen opens means an opener was lost and the extra closer is its symptom, so the repair declines and the gate still rejects. Pinned by a suite weighted toward the refusal cases, because the naive version of this fix passes a test that only checks the live case and would erase the evidence of a lost opener.

Invariant 29 — Glue must be judged in rendered text, on both sides of the comparison

Invariant 2 compares a term's glued run against the glued shapes the source already had, which is the correct rule. It was applied to raw markup, and that quietly broke it in both directions. A protected term always sits inside its own protective wrapper, so when the model fuses the next word to it the fusion lands after the closing tag, where a scan over markup stops. And an identifier the author fused on purpose — written by hand with a protective wrapper in the middle of it — reads as new glue on the source side.

So the same check simultaneously invented defects and hid them. It reported a comfortable zero from August to October while 125 genuinely glued terms sat on the published corpus, including German text a reader can simply see. The lesson generalises past this engine: a zero from a check that cannot fail is indistinguishable from a zero from a clean corpus. A half-fix is what made the zero look earned — correcting only the source side still measures zero, which is why the gap survived six weeks after the principle was already written down in the code.

Enforced by reducing both sides to the text a reader sees before any comparison: inline elements vanish with no separator, every other element becomes a single space, so a table-cell boundary stays a word break and cell adjacency invents nothing. Measured before the change rather than after: of the repairs the wider scope enables, 98 restore a real lost space and 9 would have split an authored identifier. Pinned by tests in both directions, including the defect itself so that a revert cannot pass silently.

Invariant 30 — An authored identifier must be repaired or refused, never silently mangled

Seeing more is only an improvement if the repair that follows is right. One live finding class is not a lost space at all but a corrupted name: the model translated the tail of an identifier, so TranslateS3AccessRole shipped as TranslateS3AccessRolle in nine languages. Inserting a space there produces TranslateS3AccessRo lle — measured, that is exactly where the repair's authored-prefix walk lands — which trades a loud, catchable defect for a quiet published one.

Enforced by a refusal rule in the full-document repair: a run whose authored identifier is not intact is reported and left alone, so the gate hard-fails the pair instead of publishing a mangled name. The discriminator is containment rather than a shared prefix, and getting that wrong is easy — an authored compound that survives whole with a translated word glued onto its end is repairable, and a prefix-only rule refuses it. The rule deliberately does not live in the shared position-finding helper, whose verdict must stay identical to the gate's so that a correct scaffolding strip can never hard-fail a paid translation.

And the inverse defect has no check at all. An authored identifier can also be split: BedrockCostForTokens publishes as two words in all nine space-delimited languages, and FlushToElastiCache in one. Every existing check passes it by construction — presence is a substring test, which a split satisfies, and the spacing checks only fire when a run is longer than the term, where a split makes it exactly equal. Both identifiers are now protected terms so the model cannot split them again, and one needed its English markup rewrapped because the protection pass matches within a single text node and could not see an identifier divided by a tag. The missing piece is a gate, and its shape is the mirror of the glue check: an authored run present in the source whose characters appear in the output separated by whitespace. It was found by pre-flighting identifiers before a paid re-translation, which is the finding as much as the defect is.

20.2a Pipeline Tracing — Seeing the Path a Document Actually Took

The tooling that finally separated a genuine cause from a plausible-looking symptom didn't exist yet when the search for Invariant 17's fault began. Two build-and-deploy cycles were spent chasing a caption because there was no way to see, cheaply, what any single pipeline step had actually done to the document passing through it — Aurora records what was processed, not what happened at the moment it was processed. This is the instrument that fills that gap, and it exists because reasoning from a symptom cost two deploy cycles before it caught the actual defect.

It is deliberately not a new gate, not a new audit leg, and not anything that runs unless asked. Off means zero work, by design. Every entry point returns immediately when the switch is unset, so a normal translation carries no cost from this existing at all — the exact discipline behind not adding anything that recurs on a clock. Turning it on is a single environment variable, and it works identically whether the worker is running locally or inside the ECS container, so the same switch answers the same question wherever the failure actually lives.

FlagEffect
TBI_TRACE_TAIL=1Enables tracing. The only switch normally needed.
TBI_TRACE_DUMP=1Additionally writes the full before/after HTML of any step that introduces glue, plus the raw source, to disk.
TBI_TRACE_DIRWhere trace files are written (default /tmp).

What it reports, per pipeline step: a glue finding before and after the step, computed with the exact same rule and baseline the real gate uses, so a step that introduces glue is named outright instead of inferred from which step ran most recently. A length delta. And counts of the inline elements that carry this defect class most often — a recovery mechanism that re-injects content as an appended pile shows up here as a count that moves without the surrounding prose moving with it, which is precisely how Invariant 17's root cause was finally identified rather than merely located.

Per translated part, before any placeholder restore runs, it also records how many placeholder tokens of each opaque format were sent to the model against how many came back — the single measurement that distinguished a part the model damaged from a part the engine's own recovery later mishandled, which nothing else in the pipeline records at all.

It cannot break a translation, and that guarantee is structural rather than a promise. Every public function is wrapped so an exception raised inside tracing is caught and reported, never allowed to propagate — a diagnostic capable of failing a paid-for job would be worse than no diagnostic. This is an on-demand instrument, not a scheduled check: it earns its place by having already found a real defect two other approaches missed, and it costs nothing on every day it is not needed.

Diagram 20.1: Where Each Invariant Is Enforced

flowchart TD
    P[Pre-build gate: regression suite + engine source audit] --> A
    A[Authored document from S3] --> B["Source validation: repairs always, rejects internal jobs only"]
    B --> C[Snapshot authored baseline]
    C --> D[Enrich: wrap terms, protect code, absolutise paths]
    D --> E[Split on chunk markers]
    E --> F[Per chunk: extract elements to placeholders]
    F --> G[Bedrock translation]
    G --> H[Per chunk: restore placeholders]
    H --> I[Reassemble chunks]
    I --> J[Full-document gates]
    J --> K[Final transformations]
    K --> L[Differential tail gate]
    L --> M[Write to S3]
    M --> N[Out-of-band corpus audits]
    I1[Inv 1 and 3: boundary whitespace] --- E
    I4[Inv 4: safe marker position] --- E
    I8[Inv 8: reserve literal indices] --- F
    I9[Inv 9: SVG bytes before parsing] --- F
    I7[Inv 7: degeneration guard] --- G
    I16[Inv 16: truncation gate] --- G
    I17[Inv 17: placeholder-format survival] --- G
    I6[Inv 6: three-tier recovery] --- H
    I11[Inv 11: one entry one copy] --- H
    I14[Inv 14: fragment identity] --- H
    I15[Inv 15: adjacent-splice glue guard] --- H
    I2["Inv 2: term spacing, skipped for the 8 word_spaced=false languages"] --- J
    X1["Customer job: definitive source equals target, pair skipped, lang_rejected target_is_source"] --- G
    X2["Customer job: fail-closed cancel check before publish"] --- M
    I10[Inv 10: bounded scaffolding removal] --- K
    I18[Inv 18: placeholder-residue strip-and-warn] --- L
    I12[Inv 12: validate shipped bytes] --- L
    I5[Inv 5: measure the corpus] --- N
    I13[Inv 13: no regex in engine source] --- P
    style P fill:#064e3b,stroke:#10b981,color:#e0e0e0
    style A fill:#1e3a5f,stroke:#60a5fa,color:#e0e0e0
    style G fill:#7c2d12,stroke:#f97316,color:#e0e0e0
    style J fill:#3f3f46,stroke:#a1a1aa,color:#e0e0e0
    style K fill:#3f3f46,stroke:#a1a1aa,color:#e0e0e0
    style L fill:#064e3b,stroke:#10b981,color:#e0e0e0
    style M fill:#1e3a5f,stroke:#60a5fa,color:#e0e0e0
    style N fill:#064e3b,stroke:#10b981,color:#e0e0e0
    style X1 fill:#92400e,stroke:#fbbf24,color:#e0e0e0
    style X2 fill:#7f1d1d,stroke:#f87171,color:#e0e0e0
        

The diagram makes one thing plain that a list cannot: the invariants cluster where the artifact changes hands. Extraction, generation, restoration and the final transformation sequence are where content is created, replaced or removed, and that is where every enforcement point sits. The out-of-band audits at the end exist because a pipeline cannot certify its own output — only the published bytes can do that. Generation gained two more clusters since this diagram was first drawn: a truncation gate that reads the model's own stop signal, and a format-survival gate watching a class of placeholder the rest of the pipeline treats as opaque. Restoration gained two of its own: a check that a translated fragment landed in the slot it was translated for, and a guard at every splice point ensuring two individually correct restorations don't touch with no separator between them.

20.3 The Four Content Defect Classes

Content defects sort into four classes, and the distinction is operational rather than academic: each needs a different detector and a different response. A document can be structurally valid, correctly scripted, free of glued terms, and still be wrong in a way that only one of these four finds.

ClassWhat it looks likeDetected byCorrect response
Degeneration A solid run of one repeated character filling the output budget Run length on the published bytes, never script identity Re-translate the document after isolating the dense region behind its own marker
Wrong-language prose A document under one language path whose prose is materially another language Absolute character counts per script on the published bytes Re-translate that pair
Absence Structurally valid, coherent, correctly scripted — and an older revision of itself with whole sections missing Size normalised against each language's own median expansion Depends on cause — see below
Identifier corruption Structurally valid, correctly scripted, free of glued terms — and naming a resource that does not exist, because the model translated part of an identifier or split one into two words Comparing a term's full run against the runs the English source authored, in rendered text. A split is invisible to a presence check, which a substring satisfies, and to a spacing check, which only fires on a run longer than the term Protect the whole identifier as a term so it cannot be divided again, then re-translate. Never "repair" it with a space — see Invariant 30

The absence class carries the subtlest trap in the whole detection story. Size must be normalised against each language's own typical expansion, never against English. A language that normally expands to roughly 140 percent of the English source and measures 100 percent is missing content — while looking perfectly healthy to a check that compares against English alone. Comparing against English produced a false all-clear on the first measurement.

And absence has two causes that need opposite responses. A stale revision is fixed by re-translating that document. A term that failed to preserve at otherwise-normal document size is not fixed by re-translation — it recurs, and multi-word terms dominate that bucket. Treating the second as the first burns tokens and changes nothing.

A generation-time guard and an audit are not substitutes for each other. The degeneration guard stops a collapsed chunk from ever publishing. The corpus audit reads what did publish. The guard covers everything translated from now on, the audit covers everything already live, and a job's reported status is not the same claim as the document being correct. Both are required, and neither replaces the other.

20.4 The Regression Suite and the Pre-Build Gate

Thirty-eight test files sit alongside the worker, and the build refuses to produce an image if any of them fails. That refusal is the point: before it existed, running the suite before a deploy was a discipline someone had to remember rather than a property the tooling enforced. The count has more than doubled since this section was first written — new invariants arrive with their own tests, never as an addition to an existing one, and the suite's growth is the visible record of that discipline holding.

The runner discovers test files by glob rather than from a list. That is the same "derive it, do not hardcode it" discipline applied elsewhere in this fleet's tooling, and it exists because a hand-maintained list of checks has gone stale here more than once. Adding a test file requires no edit to the runner.

Two properties of these tests are worth stating, because they are what makes the suite trustworthy rather than merely present.

Every fix carries a negative control for the case a naive version would get wrong. Not a test proving the original bug is caught — a test proving the plausible wrong fix is rejected. Several of these were written before the corresponding fix and caught a genuine flaw in the first attempt: the scaffolding removal's count-limited draft preserved the right number of attributes on the wrong elements, and only a control written in advance surfaced it.

Several tests assert that a check can fail. They reproduce the pre-fix behaviour and assert it produces the wrong answer. Without that, a green run cannot distinguish "the fix works" from "these assertions are incapable of failing" — and this codebase has a documented history of checks that returned a clean, confident, wrong answer.

A verification predicate needs verifying too. Four separate measurement errors during this work were caught only by running a control rather than by reasoning: a substring check that matched its own explanatory comment, a fingerprint that occurred three times in the control output and was therefore never evidence of anything, a guard that passed against an empty manifest and proved nothing, and a count asserted from memory that was wrong in both directions. In each case the tool answered confidently. The control is what made the answer trustworthy.

20.5 Known Boundaries

Stating what these guarantees do not cover is part of the guarantee. An overstated boundary is how a clean audit becomes a false all-clear.

What this section is really documenting. Every invariant here was found by measuring the published artifact rather than by reading the code — and in several cases the code reading produced a confident wrong answer that the measurement then corrected. Two of them were traced to functions that do not execute at all. The habit that found them is cheap and worth more than any single guard on this list: read the published bytes first, and check whether the code you are reasoning about actually runs.

Quick Reference

ItemValue
Modelqwen.qwen3-235b-a22b-2507-v1:0 (Qwen3-235B — all languages)
Failover Regionsus-east-2 → us-east-1 → us-west-2
Target Languages11 internal: es, pt, fr, de, ru, hi, ja, zh, ar, ur, it · 21 supported (TBTS customers)
Worker RuntimePython 3.11 (ECS Fargate persistent service, auto-scaling 1→11)
FinalizationPOST /admin/translate/complete/{job_id} (Go, in-process on LPO server)
Template Variable Delimiter[brackets] — never {braces} (models corrupt braces)
Worker Resources2 vCPU / 6 GB per container (Fargate — no timeout ceiling)
Memory (Lambdas)1770 MB
Worker TimeoutNone (runs to completion)
Finalize Timeout180s
Deploy Timeout60s
Max Docs per Request6
Max Active Jobs3
Daily Dollar Cap$600 (24h TTL auto-reset)
Daily Token Cap50M combined tokens (24h TTL auto-reset)
Chunk Size (Latin scripts)6000 chars
Chunk Size (CJK + Russian)4500 chars (ja, zh, ru)
Chunk Size (Indic + Arabic)3000 chars (hi, ur, ar)
Retries per Chunk3
Max Part Size24 KB (safety valve — prevents model output truncation)
MaxConcurrency (per-language)Unlimited — LPO sends one SQS message per language, auto-scales workers to match
ECR Repositorytbi-translate-worker
SQS Queuetrinity-beast-translation-queue
IAM Role (Worker)tbi-translate-role
Auto-ScalingDemand-driven: 1 (idle) → N (job submitted, N = language count) → 1 (queue empty)
ECS Servicetbi-translate-worker-service (persistent, always-on)
FinalizationPOST /admin/translate/complete/{job_id} (LPO server endpoint)
Valkey Keystx:job:{id}, tx:active, tx:history, tx:idempotency:{key}, autoops:bedrock:spend:daily, autoops:bedrock:tokens:input:daily, autoops:bedrock:tokens:output:daily
Aurora Tablestranslation_jobs, translation_job_events
Delta Metadatadocs/delta/{doc}.{lang}.json (S3)
Delta CLIbash scripts/kcc.sh delta-diff, bash scripts/kcc.sh delta-translate, bash scripts/kcc.sh delta-validate, bash scripts/kcc.sh chunk-size
CloudWatch NamespaceTBI/Translation