Spaces:
Running
Running
R6A-R4-R4 four-language translatable-payload gate and async progress observability
#42
by GuXiaopeng - opened
core/generation_runner.py
CHANGED
|
@@ -183,6 +183,12 @@ class GenerationRunner:
|
|
| 183 |
self._translation_worker_collected = 0
|
| 184 |
self._translation_worker_pending = set()
|
| 185 |
self._translation_worker_context = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
self._qualitative_quote_sources = {}
|
| 187 |
if self.preview_token:
|
| 188 |
preview_dir = Path(__file__).resolve().parents[1] / "static" / "previews"
|
|
@@ -711,6 +717,40 @@ class GenerationRunner:
|
|
| 711 |
raise RuntimeError(f"Unresolved target-language qualitative quote tags: {unresolved[:8]}")
|
| 712 |
return value, ledger
|
| 713 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 714 |
def _write_preview_payload(self, *, force: bool = False) -> None:
|
| 715 |
with self._preview_lock:
|
| 716 |
if self.preview_path is None:
|
|
@@ -731,6 +771,7 @@ class GenerationRunner:
|
|
| 731 |
"target_english_commit_text": self._preview_visible_text(
|
| 732 |
self._preview_target_english_commit_text
|
| 733 |
),
|
|
|
|
| 734 |
"updated_at": datetime.now(timezone.utc).isoformat(),
|
| 735 |
}
|
| 736 |
temporary = self.preview_path.with_suffix(".json.tmp")
|
|
@@ -874,7 +915,18 @@ class GenerationRunner:
|
|
| 874 |
'adaptive_structure': list(adaptive_structure or []),
|
| 875 |
'target_lang': str(target_lang or ''),
|
| 876 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 877 |
self._translation_worker = _OrderedTranslationWorker(self._run_translation_job)
|
|
|
|
| 878 |
|
| 879 |
def _run_translation_job(self, job: dict[str, Any]) -> dict[str, Any]:
|
| 880 |
if self.translation_agent is None:
|
|
@@ -912,6 +964,14 @@ class GenerationRunner:
|
|
| 912 |
source_hash = self._text_hash(source)
|
| 913 |
fact_ledger_hash = self._translation_ledger_hash(agent, source)
|
| 914 |
existing = dict((translation_sections or {}).get(unit_key) or {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 915 |
if (
|
| 916 |
existing.get('qa_status') in {'passed', 'passed_with_warning'}
|
| 917 |
and existing.get('source_hash') == source_hash
|
|
@@ -924,6 +984,7 @@ class GenerationRunner:
|
|
| 924 |
pending_key = (str(unit_key), source_hash, fact_ledger_hash)
|
| 925 |
if pending_key in self._translation_worker_pending:
|
| 926 |
return False
|
|
|
|
| 927 |
future = self._translation_worker.submit({
|
| 928 |
'unit_key': str(unit_key),
|
| 929 |
'unit_name': str(unit_name),
|
|
@@ -937,6 +998,7 @@ class GenerationRunner:
|
|
| 937 |
'pending_key': pending_key,
|
| 938 |
'future': future,
|
| 939 |
})
|
|
|
|
| 940 |
return True
|
| 941 |
|
| 942 |
def _collect_translation_results(self, translation_sections, translation_glossary, *, block=False):
|
|
@@ -950,6 +1012,7 @@ class GenerationRunner:
|
|
| 950 |
result = future.result()
|
| 951 |
sections = dict(result.get('translation_sections') or sections)
|
| 952 |
glossary = dict(result.get('translation_glossary') or glossary)
|
|
|
|
| 953 |
self._translation_worker_pending.discard(item['pending_key'])
|
| 954 |
self._translation_worker_collected += 1
|
| 955 |
return sections, glossary
|
|
@@ -973,6 +1036,12 @@ class GenerationRunner:
|
|
| 973 |
contract_version = TRANSLATION_CONTRACT_VERSION
|
| 974 |
fact_ledger_hash = self._translation_ledger_hash(agent, source)
|
| 975 |
existing = dict((translation_sections or {}).get(unit_key) or {})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 976 |
if (
|
| 977 |
existing.get('qa_status') in {'passed', 'passed_with_warning'}
|
| 978 |
and existing.get('source_hash') == source_hash
|
|
@@ -981,7 +1050,9 @@ class GenerationRunner:
|
|
| 981 |
and existing.get('fact_ledger_hash') == fact_ledger_hash
|
| 982 |
and existing.get('translated_text')
|
| 983 |
):
|
|
|
|
| 984 |
return translation_sections, translation_glossary
|
|
|
|
| 985 |
partial = self._assemble_partial_translation(translation_sections, adaptive_structure, include_front=include_front)
|
| 986 |
self._publish_target_preview(
|
| 987 |
partial, f'translating:{unit_name}', force=True, complete=False,
|
|
@@ -1089,6 +1160,8 @@ class GenerationRunner:
|
|
| 1089 |
'Translation deferred until final gate | unit=%s code=%s error=%s',
|
| 1090 |
unit_name, error_code, type(error).__name__,
|
| 1091 |
)
|
|
|
|
|
|
|
| 1092 |
self._publish_target_preview(
|
| 1093 |
partial, f'translation_repair_pending:{unit_name}', force=True, complete=False,
|
| 1094 |
english_commit_text=english_commit_text,
|
|
@@ -1109,6 +1182,8 @@ class GenerationRunner:
|
|
| 1109 |
'completed_at': datetime.now(timezone.utc).isoformat(),
|
| 1110 |
}
|
| 1111 |
partial = self._assemble_partial_translation(updated_sections, adaptive_structure, include_front=include_front)
|
|
|
|
|
|
|
| 1112 |
self._publish_target_preview(
|
| 1113 |
partial, f'translation_ready:{unit_name}', force=True, complete=False,
|
| 1114 |
english_commit_text=english_commit_text,
|
|
|
|
| 183 |
self._translation_worker_collected = 0
|
| 184 |
self._translation_worker_pending = set()
|
| 185 |
self._translation_worker_context = {}
|
| 186 |
+
# Display-only observability. These counters never decide task success,
|
| 187 |
+
# checkpoint authority, quota, or translation reuse.
|
| 188 |
+
self._translation_progress_sections = {}
|
| 189 |
+
self._translation_progress_queued = set()
|
| 190 |
+
self._translation_progress_inflight = set()
|
| 191 |
+
self._translation_progress_total = 0
|
| 192 |
self._qualitative_quote_sources = {}
|
| 193 |
if self.preview_token:
|
| 194 |
preview_dir = Path(__file__).resolve().parents[1] / "static" / "previews"
|
|
|
|
| 717 |
raise RuntimeError(f"Unresolved target-language qualitative quote tags: {unresolved[:8]}")
|
| 718 |
return value, ledger
|
| 719 |
|
| 720 |
+
def _translation_progress_snapshot(self) -> dict[str, int]:
|
| 721 |
+
"""Return non-authoritative logical target-unit progress for the UI."""
|
| 722 |
+
mapping = dict(getattr(self, '_translation_progress_sections', {}) or {})
|
| 723 |
+
context = getattr(self, '_translation_worker_context', {}) or {}
|
| 724 |
+
tasks = context.get('adaptive_structure') or [] if isinstance(context, dict) else []
|
| 725 |
+
logical_keys = []
|
| 726 |
+
if tasks:
|
| 727 |
+
logical_keys = ['__front_matter__'] + [
|
| 728 |
+
key for key, _title in self._unique_top_level_sections(tasks)
|
| 729 |
+
]
|
| 730 |
+
logical_keys = list(dict.fromkeys(key for key in logical_keys if key))
|
| 731 |
+
total = int(getattr(self, '_translation_progress_total', 0) or len(logical_keys))
|
| 732 |
+
if total <= 0 and logical_keys:
|
| 733 |
+
total = len(logical_keys)
|
| 734 |
+
qa_passed = 0
|
| 735 |
+
repair_pending = 0
|
| 736 |
+
inflight = set(getattr(self, '_translation_progress_inflight', set()) or set())
|
| 737 |
+
for key in logical_keys:
|
| 738 |
+
record = mapping.get(key) or {}
|
| 739 |
+
status = str(record.get('qa_status') or '')
|
| 740 |
+
if key not in inflight and status in {'passed', 'passed_with_warning'} and record.get('translated_text'):
|
| 741 |
+
qa_passed += 1
|
| 742 |
+
elif status == 'repair_pending':
|
| 743 |
+
repair_pending += 1
|
| 744 |
+
queued = len(set(getattr(self, '_translation_progress_queued', set()) or set()).intersection(logical_keys))
|
| 745 |
+
in_flight = len(inflight.intersection(logical_keys))
|
| 746 |
+
return {
|
| 747 |
+
'qa_passed': min(qa_passed, total) if total else qa_passed,
|
| 748 |
+
'repair_pending': min(repair_pending, total) if total else repair_pending,
|
| 749 |
+
'queued': min(queued, total) if total else queued,
|
| 750 |
+
'in_flight': min(in_flight, total) if total else in_flight,
|
| 751 |
+
'total': max(0, total),
|
| 752 |
+
}
|
| 753 |
+
|
| 754 |
def _write_preview_payload(self, *, force: bool = False) -> None:
|
| 755 |
with self._preview_lock:
|
| 756 |
if self.preview_path is None:
|
|
|
|
| 771 |
"target_english_commit_text": self._preview_visible_text(
|
| 772 |
self._preview_target_english_commit_text
|
| 773 |
),
|
| 774 |
+
"target_progress": self._translation_progress_snapshot(),
|
| 775 |
"updated_at": datetime.now(timezone.utc).isoformat(),
|
| 776 |
}
|
| 777 |
temporary = self.preview_path.with_suffix(".json.tmp")
|
|
|
|
| 915 |
'adaptive_structure': list(adaptive_structure or []),
|
| 916 |
'target_lang': str(target_lang or ''),
|
| 917 |
}
|
| 918 |
+
logical_keys = ['__front_matter__'] + [
|
| 919 |
+
key for key, _title in self._unique_top_level_sections(adaptive_structure)
|
| 920 |
+
]
|
| 921 |
+
logical_keys = list(dict.fromkeys(key for key in logical_keys if key))
|
| 922 |
+
self._translation_progress_total = len(logical_keys)
|
| 923 |
+
self._translation_progress_sections = dict(translation_sections or {})
|
| 924 |
+
self._translation_progress_queued = {
|
| 925 |
+
key for key in logical_keys if key in self._translation_progress_sections
|
| 926 |
+
}
|
| 927 |
+
self._translation_progress_inflight = set()
|
| 928 |
self._translation_worker = _OrderedTranslationWorker(self._run_translation_job)
|
| 929 |
+
self._write_preview_payload(force=True)
|
| 930 |
|
| 931 |
def _run_translation_job(self, job: dict[str, Any]) -> dict[str, Any]:
|
| 932 |
if self.translation_agent is None:
|
|
|
|
| 964 |
source_hash = self._text_hash(source)
|
| 965 |
fact_ledger_hash = self._translation_ledger_hash(agent, source)
|
| 966 |
existing = dict((translation_sections or {}).get(unit_key) or {})
|
| 967 |
+
if not isinstance(getattr(self, '_translation_progress_queued', None), set):
|
| 968 |
+
self._translation_progress_queued = set()
|
| 969 |
+
if not isinstance(getattr(self, '_translation_progress_inflight', None), set):
|
| 970 |
+
self._translation_progress_inflight = set()
|
| 971 |
+
# Do not replace the worker-owned display snapshot with a possibly stale
|
| 972 |
+
# main-thread copy here; collection or the worker completion path updates
|
| 973 |
+
# _translation_progress_sections monotonically.
|
| 974 |
+
self._translation_progress_queued.add(str(unit_key))
|
| 975 |
if (
|
| 976 |
existing.get('qa_status') in {'passed', 'passed_with_warning'}
|
| 977 |
and existing.get('source_hash') == source_hash
|
|
|
|
| 984 |
pending_key = (str(unit_key), source_hash, fact_ledger_hash)
|
| 985 |
if pending_key in self._translation_worker_pending:
|
| 986 |
return False
|
| 987 |
+
self._translation_progress_inflight.add(str(unit_key))
|
| 988 |
future = self._translation_worker.submit({
|
| 989 |
'unit_key': str(unit_key),
|
| 990 |
'unit_name': str(unit_name),
|
|
|
|
| 998 |
'pending_key': pending_key,
|
| 999 |
'future': future,
|
| 1000 |
})
|
| 1001 |
+
self._write_preview_payload(force=True)
|
| 1002 |
return True
|
| 1003 |
|
| 1004 |
def _collect_translation_results(self, translation_sections, translation_glossary, *, block=False):
|
|
|
|
| 1012 |
result = future.result()
|
| 1013 |
sections = dict(result.get('translation_sections') or sections)
|
| 1014 |
glossary = dict(result.get('translation_glossary') or glossary)
|
| 1015 |
+
self._translation_progress_sections = dict(sections)
|
| 1016 |
self._translation_worker_pending.discard(item['pending_key'])
|
| 1017 |
self._translation_worker_collected += 1
|
| 1018 |
return sections, glossary
|
|
|
|
| 1036 |
contract_version = TRANSLATION_CONTRACT_VERSION
|
| 1037 |
fact_ledger_hash = self._translation_ledger_hash(agent, source)
|
| 1038 |
existing = dict((translation_sections or {}).get(unit_key) or {})
|
| 1039 |
+
if not isinstance(getattr(self, '_translation_progress_queued', None), set):
|
| 1040 |
+
self._translation_progress_queued = set()
|
| 1041 |
+
if not isinstance(getattr(self, '_translation_progress_inflight', None), set):
|
| 1042 |
+
self._translation_progress_inflight = set()
|
| 1043 |
+
self._translation_progress_sections = dict(translation_sections or {})
|
| 1044 |
+
self._translation_progress_queued.add(str(unit_key))
|
| 1045 |
if (
|
| 1046 |
existing.get('qa_status') in {'passed', 'passed_with_warning'}
|
| 1047 |
and existing.get('source_hash') == source_hash
|
|
|
|
| 1050 |
and existing.get('fact_ledger_hash') == fact_ledger_hash
|
| 1051 |
and existing.get('translated_text')
|
| 1052 |
):
|
| 1053 |
+
self._translation_progress_inflight.discard(str(unit_key))
|
| 1054 |
return translation_sections, translation_glossary
|
| 1055 |
+
self._translation_progress_inflight.add(str(unit_key))
|
| 1056 |
partial = self._assemble_partial_translation(translation_sections, adaptive_structure, include_front=include_front)
|
| 1057 |
self._publish_target_preview(
|
| 1058 |
partial, f'translating:{unit_name}', force=True, complete=False,
|
|
|
|
| 1160 |
'Translation deferred until final gate | unit=%s code=%s error=%s',
|
| 1161 |
unit_name, error_code, type(error).__name__,
|
| 1162 |
)
|
| 1163 |
+
self._translation_progress_sections = dict(updated_sections)
|
| 1164 |
+
self._translation_progress_inflight.discard(str(unit_key))
|
| 1165 |
self._publish_target_preview(
|
| 1166 |
partial, f'translation_repair_pending:{unit_name}', force=True, complete=False,
|
| 1167 |
english_commit_text=english_commit_text,
|
|
|
|
| 1182 |
'completed_at': datetime.now(timezone.utc).isoformat(),
|
| 1183 |
}
|
| 1184 |
partial = self._assemble_partial_translation(updated_sections, adaptive_structure, include_front=include_front)
|
| 1185 |
+
self._translation_progress_sections = dict(updated_sections)
|
| 1186 |
+
self._translation_progress_inflight.discard(str(unit_key))
|
| 1187 |
self._publish_target_preview(
|
| 1188 |
partial, f'translation_ready:{unit_name}', force=True, complete=False,
|
| 1189 |
english_commit_text=english_commit_text,
|
core/pipeline_agent.py
CHANGED
|
@@ -4873,6 +4873,32 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4873 |
parsed[match.group(1)] = translated
|
| 4874 |
return parsed
|
| 4875 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4876 |
def _validate_translation_block(self, source, translated, block_id, target_lang):
|
| 4877 |
"""Hierarchical multilingual block assurance.
|
| 4878 |
|
|
@@ -4883,10 +4909,22 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4883 |
source_compact = re.sub(r'\s+', '', str(source or ''))
|
| 4884 |
target_compact = re.sub(r'\s+', '', str(translated or ''))
|
| 4885 |
profile = self._translation_language_profile(target_lang)
|
|
|
|
|
|
|
| 4886 |
evidence = []
|
| 4887 |
|
| 4888 |
if source_compact and source_compact.casefold() == target_compact.casefold():
|
| 4889 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4890 |
|
| 4891 |
if len(source_compact) >= 120:
|
| 4892 |
minimum = float(APP_CONFIG.get(profile['surface_min_key'], profile['surface_min_default']))
|
|
@@ -4897,7 +4935,10 @@ Translate headings and terminology naturally. Preserve standard variable abbrevi
|
|
| 4897 |
elif ratio > maximum:
|
| 4898 |
evidence.append({'code': 'SURFACE_RATIO_HIGH', 'severity': 'diagnostic', 'ratio': round(ratio, 4), 'threshold': maximum})
|
| 4899 |
|
| 4900 |
-
|
|
|
|
|
|
|
|
|
|
| 4901 |
if profile['language_code'] == 'zh':
|
| 4902 |
han = len(re.findall(r'[\u4e00-\u9fff]', str(translated or '')))
|
| 4903 |
if han == 0:
|
|
|
|
| 4873 |
parsed[match.group(1)] = translated
|
| 4874 |
return parsed
|
| 4875 |
|
| 4876 |
+
@classmethod
|
| 4877 |
+
def _translation_translatable_payload_profile(cls, source):
|
| 4878 |
+
"""Classify source content left after Translation-IR immutable anchors.
|
| 4879 |
+
|
| 4880 |
+
Equality is only suspicious when the source still contains lexical
|
| 4881 |
+
payload after server-owned immutable facts/names/IDs/formulas have been
|
| 4882 |
+
replaced by SP-T anchors. This classifier is source-side and therefore
|
| 4883 |
+
shared by Chinese, Russian, and Kazakh without language-specific
|
| 4884 |
+
exceptions. English remains the frozen authority.
|
| 4885 |
+
"""
|
| 4886 |
+
ir = cls._compile_translation_ir(source)
|
| 4887 |
+
protected = str(ir.get('protected_source') or '')
|
| 4888 |
+
residual = re.sub(r'\[\[SP-(?:C\d{6}|T\d{6})\]\]', ' ', protected)
|
| 4889 |
+
# Markdown structure and punctuation carry no lexical translation
|
| 4890 |
+
# obligation by themselves. Unicode alphabetic runs are retained so
|
| 4891 |
+
# mixed academic text is never treated as immutable merely because it
|
| 4892 |
+
# is short or contains protected objects.
|
| 4893 |
+
tokens = re.findall(r"[^\W\d_]+(?:['’\-][^\W\d_]+)*", residual, flags=re.UNICODE)
|
| 4894 |
+
lexical = [token for token in tokens if any(char.isalpha() for char in token)]
|
| 4895 |
+
return {
|
| 4896 |
+
'has_translatable_payload': bool(lexical),
|
| 4897 |
+
'payload_tokens': len(lexical),
|
| 4898 |
+
'payload_chars': sum(sum(1 for char in token if char.isalpha()) for token in lexical),
|
| 4899 |
+
'protected_anchors': len(ir.get('fact_ledger') or {}),
|
| 4900 |
+
}
|
| 4901 |
+
|
| 4902 |
def _validate_translation_block(self, source, translated, block_id, target_lang):
|
| 4903 |
"""Hierarchical multilingual block assurance.
|
| 4904 |
|
|
|
|
| 4909 |
source_compact = re.sub(r'\s+', '', str(source or ''))
|
| 4910 |
target_compact = re.sub(r'\s+', '', str(translated or ''))
|
| 4911 |
profile = self._translation_language_profile(target_lang)
|
| 4912 |
+
payload_profile = self._translation_translatable_payload_profile(source)
|
| 4913 |
+
has_translatable_payload = bool(payload_profile.get('has_translatable_payload'))
|
| 4914 |
evidence = []
|
| 4915 |
|
| 4916 |
if source_compact and source_compact.casefold() == target_compact.casefold():
|
| 4917 |
+
if has_translatable_payload:
|
| 4918 |
+
evidence.append({
|
| 4919 |
+
'code': 'UNCHANGED_SOURCE', 'severity': 'repair',
|
| 4920 |
+
'payload_tokens': int(payload_profile.get('payload_tokens') or 0),
|
| 4921 |
+
'payload_chars': int(payload_profile.get('payload_chars') or 0),
|
| 4922 |
+
})
|
| 4923 |
+
else:
|
| 4924 |
+
evidence.append({
|
| 4925 |
+
'code': 'UNCHANGED_IMMUTABLE_ONLY', 'severity': 'diagnostic',
|
| 4926 |
+
'protected_anchors': int(payload_profile.get('protected_anchors') or 0),
|
| 4927 |
+
})
|
| 4928 |
|
| 4929 |
if len(source_compact) >= 120:
|
| 4930 |
minimum = float(APP_CONFIG.get(profile['surface_min_key'], profile['surface_min_default']))
|
|
|
|
| 4935 |
elif ratio > maximum:
|
| 4936 |
evidence.append({'code': 'SURFACE_RATIO_HIGH', 'severity': 'diagnostic', 'ratio': round(ratio, 4), 'threshold': maximum})
|
| 4937 |
|
| 4938 |
+
# A block made entirely of immutable Translation-IR anchors is not
|
| 4939 |
+
# expected to acquire Han/Cyrillic characters. Script presence is
|
| 4940 |
+
# meaningful only when the English source has lexical payload.
|
| 4941 |
+
if len(target_compact) >= 40 and has_translatable_payload:
|
| 4942 |
if profile['language_code'] == 'zh':
|
| 4943 |
han = len(re.findall(r'[\u4e00-\u9fff]', str(translated or '')))
|
| 4944 |
if han == 0:
|
smartpaper_restore_bridge/frontend/index.html
CHANGED
|
@@ -386,6 +386,14 @@
|
|
| 386 |
} else if (stage === "translation_complete") {
|
| 387 |
value = String(map.complete || value);
|
| 388 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 389 |
if (!value) return;
|
| 390 |
const hasTargetText = Boolean(String(payload && payload.target_text || "").trim()) || Boolean(channel.authoritative);
|
| 391 |
if (hasTargetText) {
|
|
|
|
| 386 |
} else if (stage === "translation_complete") {
|
| 387 |
value = String(map.complete || value);
|
| 388 |
}
|
| 389 |
+
// R6A-R4-R4: keep the approved localized status/layout and append only a
|
| 390 |
+
// language-neutral logical QA-passed/total fraction. Unapproved target
|
| 391 |
+
// prose remains hidden until the existing QA publication boundary passes.
|
| 392 |
+
const targetProgress = payload && payload.target_progress && typeof payload.target_progress === "object"
|
| 393 |
+
? payload.target_progress : {};
|
| 394 |
+
const total = Math.max(0, asCount(targetProgress.total));
|
| 395 |
+
const qaPassed = Math.max(0, Math.min(total || Number.MAX_SAFE_INTEGER, asCount(targetProgress.qa_passed)));
|
| 396 |
+
if (total > 0) value = `${value} · ${qaPassed}/${total}`;
|
| 397 |
if (!value) return;
|
| 398 |
const hasTargetText = Boolean(String(payload && payload.target_text || "").trim()) || Boolean(channel.authoritative);
|
| 399 |
if (hasTargetText) {
|