blun-king-cli 9.1.597 → 9.1.600
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/README.md +17 -1
- package/agent-spine-plugin/CHANGELOG.md +1 -1
- package/agent-spine-plugin/docs/host-integration.md +1 -15
- package/agent-spine-plugin/docs/preflight-recall.md +1 -1
- package/agent-spine-plugin/scripts/check-install.js +1 -6
- package/agent-spine-plugin/scripts/hermetic-process.js +8 -0
- package/agent-spine-plugin/scripts/release-check.js +2 -3
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +8 -11
- package/agent-spine-plugin/src/hook.js +40 -40
- package/agent-spine-plugin/src/lib/briefing.js +6 -3
- package/agent-spine-plugin/src/lib/hook-context.js +63 -64
- package/agent-spine-plugin/src/lib/hook-output.js +115 -94
- package/agent-spine-plugin/src/lib/hook-timeline.js +20 -11
- package/agent-spine-plugin/src/lib/host-instruction-budget.js +8 -14
- package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +0 -7
- package/agent-spine-plugin/src/lib/pre-answer-timeline-recall.js +104 -0
- package/agent-spine-plugin/src/lib/session-timeline-codex.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +0 -2
- package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +0 -22
- package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-prior.js +42 -50
- package/agent-spine-plugin/src/lib/session-timeline-provider.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline-query.js +0 -1
- package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +0 -4
- package/agent-spine-plugin/src/lib/session-timeline-source.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline-transport.js +0 -8
- package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +0 -3
- package/agent-spine-plugin/src/lib/session-timeline.js +370 -408
- package/agent-spine-plugin/src/lib/source-roots.js +26 -16
- package/agent-spine-plugin/src/lib/timeline-user-feedback.js +0 -1
- package/bin/core-bootstrap.js +18 -10
- package/bin/launcher-mode.js +5 -0
- package/bin/launcher-runtime.js +27 -0
- package/bin/verify-bundled-agent-sources.cjs +28 -2
- package/blun.mjs +798 -143
- package/bundled-agent-sources.json +192 -155
- package/package.json +1 -1
- package/standard-skills/translate-native/README.md +1334 -1
- package/standard-skills/translate-native/SKILL.md +1 -1
- package/standard-skills/translate-native/VERSION +1 -1
- package/standard-skills/translate-native/docs/PREMORTEM.md +901 -0
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +208 -41
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1319 -77
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +66 -11
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +19 -1
- package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +24 -6
- package/standard-skills/translate-native/integrations/commercial_localization_profile.py +23 -0
- package/standard-skills/translate-native/integrations/website_localization.py +234 -10
- package/standard-skills/translate-native/integrations/website_localization_benchmark.py +39 -3
- package/standard-skills/translate-native/integrations/website_localization_cms.py +512 -10
- package/standard-skills/translate-native/integrations/website_localization_cms_client.py +116 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +101 -11
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +12 -1
- package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +186 -24
- package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +64 -1
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +528 -5
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth.py +918 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth_runtime.py +609 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_client.py +885 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_http.py +1244 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +220 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_sidecar_adapter.py +331 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_capabilities_http.py +444 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_client.py +673 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch.py +1317 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_client.py +826 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_http.py +1152 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_openapi.py +896 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_runtime.py +621 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_http.py +1180 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_runtime.py +1479 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +133 -17
- package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +319 -4
- package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +72 -2
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +13 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +56 -13
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_openapi.py +335 -0
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +66 -1
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +73 -7
- package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +93 -9
- package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +221 -11
- package/standard-skills/translate-native/integrations/website_localization_health.py +6 -0
- package/standard-skills/translate-native/integrations/website_localization_http_provider.py +1 -1
- package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +350 -0
- package/standard-skills/translate-native/integrations/website_localization_queue.py +126 -56
- package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +166 -13
- package/standard-skills/translate-native/integrations/website_localization_release.py +423 -17
- package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +168 -13
- package/standard-skills/translate-native/integrations/website_localization_runner.py +10 -0
- package/standard-skills/translate-native/integrations/website_localization_worker.py +213 -9
- package/standard-skills/translate-native/provenance.json +2 -2
- package/standard-skills/translate-native/references/commercial-localization.md +96 -17
- package/standard-skills/translate-native/scripts/blun_language_guard.py +3 -1
- package/standard-skills/translate-native/scripts/check_commercial_review.py +69 -9
- package/standard-skills/translate-native/scripts/commercial_localization_profile.py +829 -31
- package/standard-tools/language-guard/VERSION +1 -1
- package/standard-tools/language-guard/blun_language_guard.py +3 -1
- package/standard-tools/language-guard/check_commercial_review.py +69 -9
- package/standard-tools/language-guard/commercial_localization_profile.py +829 -31
- package/standard-tools/language-guard/provenance.json +2 -2
- package/standard-tools/manifest.json +1 -1
- package/worker-host.mjs +215 -82
- package/agent-spine-plugin/scripts/check-install-king.js +0 -37
package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py
CHANGED
|
@@ -10,8 +10,17 @@ from typing import Any
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
SCHEMA = "blun.website-localization-quality-profile.v1"
|
|
13
|
+
COMMERCIAL_SCHEMA = "translate-native.commercial-locale-quality-profile.v2"
|
|
14
|
+
COMMERCIAL_RENDERING_SCHEMA = "translate-native.commercial-rendering-reference.v1"
|
|
15
|
+
COMMERCIAL_RENDERING_REGISTRY_SCHEMA = (
|
|
16
|
+
"translate-native.commercial-rendering-registry.v1"
|
|
17
|
+
)
|
|
13
18
|
CLDR_VERSION = "48"
|
|
14
19
|
CLDR_SUMMARY = "https://www.unicode.org/cldr/charts/48/summary/{language}.html"
|
|
20
|
+
CLDR_NUMBERS = (
|
|
21
|
+
"https://github.com/unicode-org/cldr-json/blob/48.0.0/"
|
|
22
|
+
"cldr-json/cldr-numbers-full/main/{locale}/numbers.json"
|
|
23
|
+
)
|
|
15
24
|
MALTESE_ORTHOGRAPHY_SOURCE = (
|
|
16
25
|
"https://kunsilltalmalti.gov.mt/mistoqssija-u-twegiba-51-76/"
|
|
17
26
|
)
|
|
@@ -26,6 +35,18 @@ REQUIRED_RED_TEAM_CHECKS = (
|
|
|
26
35
|
"unnatural_cta",
|
|
27
36
|
"marketing_calque",
|
|
28
37
|
)
|
|
38
|
+
COMMERCIAL_REVIEW_CHECKS = (
|
|
39
|
+
"amount_currency",
|
|
40
|
+
"discount_basis",
|
|
41
|
+
"qualifiers",
|
|
42
|
+
"tax_status",
|
|
43
|
+
"billing_interval",
|
|
44
|
+
"commitment",
|
|
45
|
+
"renewal",
|
|
46
|
+
"cancellation",
|
|
47
|
+
"conditions",
|
|
48
|
+
"offer_assignment",
|
|
49
|
+
)
|
|
29
50
|
|
|
30
51
|
|
|
31
52
|
@dataclass(frozen=True)
|
|
@@ -44,6 +65,70 @@ class LocaleQualityProfile:
|
|
|
44
65
|
return body
|
|
45
66
|
|
|
46
67
|
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class CommercialRenderingReference:
|
|
70
|
+
locale: str
|
|
71
|
+
cldr_locale: str
|
|
72
|
+
numbering_system: str
|
|
73
|
+
native_numbering_system: str
|
|
74
|
+
minimum_grouping_digits: int
|
|
75
|
+
decimal_symbol: str
|
|
76
|
+
grouping_symbol: str
|
|
77
|
+
decimal_pattern: str
|
|
78
|
+
percent_pattern: str
|
|
79
|
+
currency_pattern: str
|
|
80
|
+
currency_alpha_pattern: str
|
|
81
|
+
currency_append_iso_pattern: str
|
|
82
|
+
approximately_pattern: str
|
|
83
|
+
at_least_pattern: str
|
|
84
|
+
at_most_pattern: str
|
|
85
|
+
range_pattern: str
|
|
86
|
+
|
|
87
|
+
def as_payload(self) -> dict[str, Any]:
|
|
88
|
+
return {
|
|
89
|
+
"schema": COMMERCIAL_RENDERING_SCHEMA,
|
|
90
|
+
"locale": self.locale,
|
|
91
|
+
"source": {
|
|
92
|
+
"authority": "Unicode CLDR",
|
|
93
|
+
"version": CLDR_VERSION,
|
|
94
|
+
"locale": self.cldr_locale,
|
|
95
|
+
"url": CLDR_NUMBERS.format(locale=self.cldr_locale),
|
|
96
|
+
},
|
|
97
|
+
"numbering_system": self.numbering_system,
|
|
98
|
+
"native_numbering_system": self.native_numbering_system,
|
|
99
|
+
"minimum_grouping_digits": self.minimum_grouping_digits,
|
|
100
|
+
"symbols": {
|
|
101
|
+
"decimal": self.decimal_symbol,
|
|
102
|
+
"group": self.grouping_symbol,
|
|
103
|
+
},
|
|
104
|
+
"patterns": {
|
|
105
|
+
"decimal": self.decimal_pattern,
|
|
106
|
+
"percent": self.percent_pattern,
|
|
107
|
+
"currency": self.currency_pattern,
|
|
108
|
+
"currency_alpha_next_to_number": self.currency_alpha_pattern,
|
|
109
|
+
"currency_append_iso": self.currency_append_iso_pattern,
|
|
110
|
+
"approximately": self.approximately_pattern,
|
|
111
|
+
"at_least": self.at_least_pattern,
|
|
112
|
+
"at_most": self.at_most_pattern,
|
|
113
|
+
"range": self.range_pattern,
|
|
114
|
+
},
|
|
115
|
+
"application": {
|
|
116
|
+
"purpose": "target-locale-rendering-guidance",
|
|
117
|
+
"semantic_proof": False,
|
|
118
|
+
"allow_equivalent_number_words": True,
|
|
119
|
+
"allow_equivalent_written_percentages": True,
|
|
120
|
+
"allow_equivalent_digit_forms": True,
|
|
121
|
+
"preserve_exact_value_and_currency_identity": True,
|
|
122
|
+
"rounding_allowed": False,
|
|
123
|
+
"currency_conversion_allowed": False,
|
|
124
|
+
"ambiguous_values": "targeted-review",
|
|
125
|
+
"unresolved_route": (
|
|
126
|
+
"independent-model-or-qualified-native-domain-review"
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
47
132
|
def _canonical_json(value: Any) -> bytes:
|
|
48
133
|
return json.dumps(
|
|
49
134
|
value,
|
|
@@ -222,9 +307,155 @@ PROFILES: tuple[LocaleQualityProfile, ...] = (
|
|
|
222
307
|
),
|
|
223
308
|
)
|
|
224
309
|
|
|
310
|
+
_NBSP = "\N{NO-BREAK SPACE}"
|
|
311
|
+
_NNBSP = "\N{NARROW NO-BREAK SPACE}"
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _rendering(
|
|
315
|
+
locale: str,
|
|
316
|
+
cldr_locale: str,
|
|
317
|
+
*,
|
|
318
|
+
minimum_grouping_digits: int = 1,
|
|
319
|
+
decimal_symbol: str = ",",
|
|
320
|
+
grouping_symbol: str = _NBSP,
|
|
321
|
+
percent_pattern: str = f"#,##0{_NBSP}%",
|
|
322
|
+
currency_pattern: str = f"#,##0.00{_NBSP}¤",
|
|
323
|
+
currency_alpha_pattern: str | None = None,
|
|
324
|
+
currency_append_iso_pattern: str = f"{{0}}{_NBSP}¤¤",
|
|
325
|
+
approximately_pattern: str = "~{0}",
|
|
326
|
+
at_least_pattern: str = "≥{0}",
|
|
327
|
+
at_most_pattern: str = "≤{0}",
|
|
328
|
+
range_pattern: str = "{0}–{1}",
|
|
329
|
+
) -> CommercialRenderingReference:
|
|
330
|
+
return CommercialRenderingReference(
|
|
331
|
+
locale=locale,
|
|
332
|
+
cldr_locale=cldr_locale,
|
|
333
|
+
numbering_system="latn",
|
|
334
|
+
native_numbering_system="latn",
|
|
335
|
+
minimum_grouping_digits=minimum_grouping_digits,
|
|
336
|
+
decimal_symbol=decimal_symbol,
|
|
337
|
+
grouping_symbol=grouping_symbol,
|
|
338
|
+
decimal_pattern="#,##0.###",
|
|
339
|
+
percent_pattern=percent_pattern,
|
|
340
|
+
currency_pattern=currency_pattern,
|
|
341
|
+
currency_alpha_pattern=currency_alpha_pattern or currency_pattern,
|
|
342
|
+
currency_append_iso_pattern=currency_append_iso_pattern,
|
|
343
|
+
approximately_pattern=approximately_pattern,
|
|
344
|
+
at_least_pattern=at_least_pattern,
|
|
345
|
+
at_most_pattern=at_most_pattern,
|
|
346
|
+
range_pattern=range_pattern,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
# CLDR 48 parent data is used unless the chosen BCP-47 profile has an explicit
|
|
351
|
+
# regional override. These are rendering references, never semantic validators.
|
|
352
|
+
COMMERCIAL_RENDERING_REFERENCES: tuple[CommercialRenderingReference, ...] = (
|
|
353
|
+
_rendering(
|
|
354
|
+
"bg-BG", "bg", minimum_grouping_digits=2,
|
|
355
|
+
percent_pattern="#,##0%", at_least_pattern="≥ {0}",
|
|
356
|
+
at_most_pattern="≤ {0}", range_pattern="{0} – {1}",
|
|
357
|
+
),
|
|
358
|
+
_rendering(
|
|
359
|
+
"hr-HR", "hr", grouping_symbol=".", at_least_pattern="{0}+",
|
|
360
|
+
range_pattern="{0} – {1}",
|
|
361
|
+
),
|
|
362
|
+
_rendering(
|
|
363
|
+
"cs-CZ", "cs", currency_append_iso_pattern="{0} ¤¤",
|
|
364
|
+
),
|
|
365
|
+
_rendering(
|
|
366
|
+
"da-DK", "da", grouping_symbol=".", at_least_pattern="{0}+",
|
|
367
|
+
range_pattern="{0}-{1}",
|
|
368
|
+
),
|
|
369
|
+
_rendering(
|
|
370
|
+
"nl-NL", "nl", grouping_symbol=".", percent_pattern="#,##0%",
|
|
371
|
+
currency_pattern=f"¤{_NBSP}#,##0.00;¤{_NBSP}-#,##0.00",
|
|
372
|
+
at_least_pattern="{0}+", range_pattern="{0}-{1}",
|
|
373
|
+
),
|
|
374
|
+
_rendering(
|
|
375
|
+
"en-IE", "en-IE", decimal_symbol=".", grouping_symbol=",",
|
|
376
|
+
percent_pattern="#,##0%", currency_pattern="¤#,##0.00",
|
|
377
|
+
currency_alpha_pattern=f"¤{_NBSP}#,##0.00",
|
|
378
|
+
at_least_pattern="{0}+",
|
|
379
|
+
),
|
|
380
|
+
_rendering(
|
|
381
|
+
"et-EE", "et", minimum_grouping_digits=2,
|
|
382
|
+
percent_pattern="#,##0%", approximately_pattern="~ {0}",
|
|
383
|
+
at_most_pattern="≤ {0}", range_pattern="{0}‒{1}",
|
|
384
|
+
),
|
|
385
|
+
_rendering("fi-FI", "fi", at_least_pattern="vähintään {0}"),
|
|
386
|
+
_rendering(
|
|
387
|
+
"fr-FR", "fr", grouping_symbol=_NNBSP,
|
|
388
|
+
approximately_pattern="≈{0}",
|
|
389
|
+
),
|
|
390
|
+
_rendering(
|
|
391
|
+
"de-AT", "de-AT", currency_pattern=f"¤{_NBSP}#,##0.00",
|
|
392
|
+
approximately_pattern="≈{0}", at_least_pattern="{0}+",
|
|
393
|
+
),
|
|
394
|
+
_rendering(
|
|
395
|
+
"el-GR", "el", grouping_symbol=".", percent_pattern="#,##0%",
|
|
396
|
+
at_least_pattern="{0}+",
|
|
397
|
+
),
|
|
398
|
+
_rendering(
|
|
399
|
+
"hu-HU", "hu", minimum_grouping_digits=2,
|
|
400
|
+
percent_pattern="#,##0%", at_least_pattern="{0}+",
|
|
401
|
+
),
|
|
402
|
+
_rendering(
|
|
403
|
+
"ga-IE", "ga", decimal_symbol=".", grouping_symbol=",",
|
|
404
|
+
percent_pattern="#,##0%", currency_pattern="¤#,##0.00",
|
|
405
|
+
currency_alpha_pattern=f"¤{_NBSP}#,##0.00",
|
|
406
|
+
at_least_pattern="{0}+",
|
|
407
|
+
),
|
|
408
|
+
_rendering(
|
|
409
|
+
"it-IT", "it", minimum_grouping_digits=2, grouping_symbol=".",
|
|
410
|
+
percent_pattern="#,##0%", range_pattern="{0}-{1}",
|
|
411
|
+
),
|
|
412
|
+
_rendering(
|
|
413
|
+
"lv-LV", "lv", minimum_grouping_digits=2,
|
|
414
|
+
percent_pattern="#,##0%",
|
|
415
|
+
),
|
|
416
|
+
_rendering("lt-LT", "lt"),
|
|
417
|
+
_rendering(
|
|
418
|
+
"mt-MT", "mt", decimal_symbol=".", grouping_symbol=",",
|
|
419
|
+
percent_pattern="#,##0%", currency_pattern="¤#,##0.00",
|
|
420
|
+
currency_alpha_pattern=f"¤{_NBSP}#,##0.00",
|
|
421
|
+
),
|
|
422
|
+
_rendering(
|
|
423
|
+
"pl-PL", "pl", minimum_grouping_digits=2,
|
|
424
|
+
percent_pattern="#,##0%",
|
|
425
|
+
),
|
|
426
|
+
_rendering(
|
|
427
|
+
"pt-PT", "pt-PT", minimum_grouping_digits=2,
|
|
428
|
+
percent_pattern="#,##0%", at_least_pattern="+{0}",
|
|
429
|
+
range_pattern="{0} - {1}",
|
|
430
|
+
),
|
|
431
|
+
_rendering(
|
|
432
|
+
"ro-RO", "ro", grouping_symbol=".",
|
|
433
|
+
range_pattern="{0} - {1}",
|
|
434
|
+
),
|
|
435
|
+
_rendering(
|
|
436
|
+
"sk-SK", "sk", at_least_pattern="{0}+",
|
|
437
|
+
range_pattern="{0} – {1}",
|
|
438
|
+
),
|
|
439
|
+
_rendering(
|
|
440
|
+
"sl-SI", "sl", minimum_grouping_digits=2, grouping_symbol=".",
|
|
441
|
+
approximately_pattern="~ {0}", at_least_pattern="≥ {0}",
|
|
442
|
+
at_most_pattern="≤ {0}",
|
|
443
|
+
),
|
|
444
|
+
_rendering(
|
|
445
|
+
"es-ES", "es", minimum_grouping_digits=2, grouping_symbol=".",
|
|
446
|
+
at_least_pattern="Más de {0}", range_pattern="{0}-{1}",
|
|
447
|
+
),
|
|
448
|
+
_rendering("sv-SE", "sv", at_least_pattern="⩾{0}"),
|
|
449
|
+
)
|
|
450
|
+
|
|
225
451
|
_BY_LOCALE = {profile.locale: profile for profile in PROFILES}
|
|
226
452
|
if len(_BY_LOCALE) != len(PROFILES):
|
|
227
453
|
raise RuntimeError("duplicate website-localization quality profile")
|
|
454
|
+
_COMMERCIAL_RENDERING_BY_LOCALE = {
|
|
455
|
+
reference.locale: reference for reference in COMMERCIAL_RENDERING_REFERENCES
|
|
456
|
+
}
|
|
457
|
+
if set(_COMMERCIAL_RENDERING_BY_LOCALE) != set(_BY_LOCALE):
|
|
458
|
+
raise RuntimeError("commercial rendering and quality-profile registries differ")
|
|
228
459
|
|
|
229
460
|
|
|
230
461
|
def quality_profile_for(locale: str) -> dict[str, Any]:
|
|
@@ -233,3 +464,122 @@ def quality_profile_for(locale: str) -> dict[str, Any]:
|
|
|
233
464
|
except (KeyError, TypeError):
|
|
234
465
|
raise ValueError("unsupported quality-profile locale") from None
|
|
235
466
|
return profile.as_payload()
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def commercial_quality_profile_for(
|
|
470
|
+
locale: str,
|
|
471
|
+
commercial_profile: str,
|
|
472
|
+
) -> dict[str, Any]:
|
|
473
|
+
"""Bind commercial evaluation to one exact locale quality generation."""
|
|
474
|
+
|
|
475
|
+
try:
|
|
476
|
+
profile = _BY_LOCALE[locale]
|
|
477
|
+
except (KeyError, TypeError):
|
|
478
|
+
raise ValueError("unsupported commercial quality-profile locale") from None
|
|
479
|
+
if (
|
|
480
|
+
not isinstance(commercial_profile, str)
|
|
481
|
+
or not commercial_profile
|
|
482
|
+
or commercial_profile != commercial_profile.strip()
|
|
483
|
+
or len(commercial_profile) > 256
|
|
484
|
+
):
|
|
485
|
+
raise ValueError("commercial profile is invalid")
|
|
486
|
+
quality = profile.as_payload()
|
|
487
|
+
rendering = _COMMERCIAL_RENDERING_BY_LOCALE[locale].as_payload()
|
|
488
|
+
body = {
|
|
489
|
+
"schema": COMMERCIAL_SCHEMA,
|
|
490
|
+
"locale": locale,
|
|
491
|
+
"version": f"commercial-eu-{locale}-2026-09-2",
|
|
492
|
+
"commercial_profile": commercial_profile,
|
|
493
|
+
"quality_profile_version": quality["version"],
|
|
494
|
+
"quality_profile_sha256": quality["sha256"],
|
|
495
|
+
"rendering_reference": rendering,
|
|
496
|
+
"creation_focus": [
|
|
497
|
+
*quality["native_review_focus"],
|
|
498
|
+
(
|
|
499
|
+
"Write prices, offers, billing intervals, commitments, renewal, "
|
|
500
|
+
"cancellation and conditions as natural native commercial copy "
|
|
501
|
+
"without changing any proposition or its offer assignment."
|
|
502
|
+
),
|
|
503
|
+
(
|
|
504
|
+
"Apply rendering_reference for target-locale number, percent, "
|
|
505
|
+
"currency, spacing, approximation, limit and range conventions; "
|
|
506
|
+
"do not round values or convert currencies."
|
|
507
|
+
),
|
|
508
|
+
],
|
|
509
|
+
"native_review_focus": [
|
|
510
|
+
*quality["native_review_focus"],
|
|
511
|
+
(
|
|
512
|
+
"Reject source-shaped price labels, interval phrases, CTAs and "
|
|
513
|
+
"terms; allow locale-appropriate number, currency, spacing and "
|
|
514
|
+
"punctuation conventions when the commercial meaning is exact."
|
|
515
|
+
),
|
|
516
|
+
(
|
|
517
|
+
"Judge numeric and currency typography against rendering_reference "
|
|
518
|
+
"while accepting natural equivalent number words, written "
|
|
519
|
+
"percentages and digit forms."
|
|
520
|
+
),
|
|
521
|
+
],
|
|
522
|
+
"fidelity_review_focus": [
|
|
523
|
+
*quality["fidelity_review_focus"],
|
|
524
|
+
(
|
|
525
|
+
"Check every commercial proposition and footnote against its own "
|
|
526
|
+
"offer, including values, limits, timing, tax status and conditions."
|
|
527
|
+
),
|
|
528
|
+
(
|
|
529
|
+
"Use rendering_reference only to interpret locale formatting, "
|
|
530
|
+
"never as deterministic proof of value equality; route ambiguous "
|
|
531
|
+
"values to targeted review."
|
|
532
|
+
),
|
|
533
|
+
],
|
|
534
|
+
"adversarial_focus": [
|
|
535
|
+
*quality["adversarial_focus"],
|
|
536
|
+
(
|
|
537
|
+
"Reject swapped offer terms, hidden qualifiers, changed billing "
|
|
538
|
+
"or commitment periods, strengthened discounts, and invented tax, "
|
|
539
|
+
"renewal or cancellation claims."
|
|
540
|
+
),
|
|
541
|
+
],
|
|
542
|
+
"required_commercial_checks": list(COMMERCIAL_REVIEW_CHECKS),
|
|
543
|
+
"source_refs": quality["source_refs"],
|
|
544
|
+
}
|
|
545
|
+
detached = json.loads(_canonical_json(body))
|
|
546
|
+
detached["sha256"] = hashlib.sha256(_canonical_json(detached)).hexdigest()
|
|
547
|
+
return detached
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def commercial_rendering_registry(commercial_profile: str) -> dict[str, Any]:
|
|
551
|
+
"""Return the complete content-free commercial rendering registry."""
|
|
552
|
+
|
|
553
|
+
entries = []
|
|
554
|
+
for profile in PROFILES:
|
|
555
|
+
commercial = commercial_quality_profile_for(
|
|
556
|
+
profile.locale,
|
|
557
|
+
commercial_profile,
|
|
558
|
+
)
|
|
559
|
+
entries.append({
|
|
560
|
+
"locale": profile.locale,
|
|
561
|
+
"commercial_quality_profile": {
|
|
562
|
+
"version": commercial["version"],
|
|
563
|
+
"sha256": commercial["sha256"],
|
|
564
|
+
},
|
|
565
|
+
"rendering_reference": commercial["rendering_reference"],
|
|
566
|
+
})
|
|
567
|
+
body = {
|
|
568
|
+
"schema": COMMERCIAL_RENDERING_REGISTRY_SCHEMA,
|
|
569
|
+
"commercial_profile": commercial_profile,
|
|
570
|
+
"source": {
|
|
571
|
+
"authority": "Unicode CLDR",
|
|
572
|
+
"version": CLDR_VERSION,
|
|
573
|
+
},
|
|
574
|
+
"content_policy": {
|
|
575
|
+
"source_text": False,
|
|
576
|
+
"target_text": False,
|
|
577
|
+
"project_prices": False,
|
|
578
|
+
"project_brands": False,
|
|
579
|
+
"credentials": False,
|
|
580
|
+
},
|
|
581
|
+
"locales": entries,
|
|
582
|
+
}
|
|
583
|
+
detached = json.loads(_canonical_json(body))
|
|
584
|
+
detached["sha256"] = hashlib.sha256(_canonical_json(detached)).hexdigest()
|
|
585
|
+
return detached
|
|
@@ -10,15 +10,18 @@ that the output is reviewed, signed, or ready for publication.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import hashlib
|
|
13
|
+
import importlib.util
|
|
13
14
|
import json
|
|
14
15
|
import re
|
|
15
16
|
import secrets
|
|
16
17
|
import sqlite3
|
|
18
|
+
import sys
|
|
17
19
|
import time
|
|
18
20
|
import unicodedata
|
|
19
21
|
from contextlib import contextmanager
|
|
20
22
|
from dataclasses import dataclass
|
|
21
|
-
from
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any, Callable, Iterator, Mapping
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
SCHEMA_VERSION = 1
|
|
@@ -26,6 +29,7 @@ MAX_ATTEMPTS = 20
|
|
|
26
29
|
MAX_LEASE_SECONDS = 86_400.0
|
|
27
30
|
MAX_PAYLOAD_BYTES = 2_000_000
|
|
28
31
|
MAX_RESULT_BYTES = 2_000_000
|
|
32
|
+
MAX_BINDING_QUARANTINE = 24
|
|
29
33
|
QUALITY_PASSES = ["target_native", "source_fidelity"]
|
|
30
34
|
ERROR_CODE = re.compile(r"^[a-z][a-z0-9_.-]{0,127}$")
|
|
31
35
|
_STATUSES = ("pending", "leased", "retry_wait", "succeeded", "failed")
|
|
@@ -49,6 +53,7 @@ _COLUMNS = (
|
|
|
49
53
|
"updated_at",
|
|
50
54
|
)
|
|
51
55
|
_PLAN_COLUMNS = ("plan_id", "job_id", "target_locale", "created_at")
|
|
56
|
+
_DEFAULT_BINDING_VALIDATOR: Callable[[dict[str, Any]], bool] | None = None
|
|
52
57
|
|
|
53
58
|
|
|
54
59
|
class LocalizationQueueBlocked(RuntimeError):
|
|
@@ -156,6 +161,32 @@ def _error(code: Any, detail: Any = None) -> tuple[str, str | None]:
|
|
|
156
161
|
return code, _hash_text(detail)
|
|
157
162
|
|
|
158
163
|
|
|
164
|
+
def _default_binding_validator() -> Callable[[dict[str, Any]], bool]:
|
|
165
|
+
"""Load the current worker validator without duplicating its contract."""
|
|
166
|
+
global _DEFAULT_BINDING_VALIDATOR
|
|
167
|
+
if _DEFAULT_BINDING_VALIDATOR is None:
|
|
168
|
+
path = Path(__file__).with_name("website_localization_worker.py")
|
|
169
|
+
spec = importlib.util.spec_from_file_location(
|
|
170
|
+
"blun_website_localization_queue_worker",
|
|
171
|
+
path,
|
|
172
|
+
)
|
|
173
|
+
if spec is None or spec.loader is None:
|
|
174
|
+
raise LocalizationQueueBlocked("current job binding validator is unavailable")
|
|
175
|
+
module = importlib.util.module_from_spec(spec)
|
|
176
|
+
sys.modules[spec.name] = module
|
|
177
|
+
spec.loader.exec_module(module)
|
|
178
|
+
|
|
179
|
+
def validate(payload: dict[str, Any]) -> bool:
|
|
180
|
+
try:
|
|
181
|
+
module._validated_job(payload)
|
|
182
|
+
except module.LocalizationWorkerBlocked:
|
|
183
|
+
return False
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
_DEFAULT_BINDING_VALIDATOR = validate
|
|
187
|
+
return _DEFAULT_BINDING_VALIDATOR
|
|
188
|
+
|
|
189
|
+
|
|
159
190
|
@contextmanager
|
|
160
191
|
def _transaction(connection: sqlite3.Connection) -> Iterator[None]:
|
|
161
192
|
if connection.in_transaction:
|
|
@@ -339,18 +370,53 @@ class LocalizationQueue:
|
|
|
339
370
|
now: float | int | None = None,
|
|
340
371
|
lease_seconds: float | int = 300,
|
|
341
372
|
eligible_plan_ids: tuple[str, ...] | None = None,
|
|
373
|
+
binding_validator: Callable[[dict[str, Any]], bool] | None = None,
|
|
342
374
|
) -> ClaimedJob | None:
|
|
343
375
|
worker_id = _field("worker_id", worker_id, limit=128)
|
|
344
376
|
now = _timestamp("now", now)
|
|
345
377
|
lease_seconds = _duration("lease_seconds", lease_seconds)
|
|
378
|
+
if binding_validator is None:
|
|
379
|
+
binding_validator = _default_binding_validator()
|
|
380
|
+
elif not callable(binding_validator):
|
|
381
|
+
raise LocalizationQueueBlocked("binding_validator must be callable")
|
|
346
382
|
if eligible_plan_ids is not None:
|
|
347
383
|
if not isinstance(eligible_plan_ids, tuple):
|
|
348
384
|
raise LocalizationQueueBlocked("eligible_plan_ids must be an immutable tuple")
|
|
349
385
|
eligible_plan_ids = tuple(sorted({
|
|
350
386
|
_field("eligible_plan_id", value) for value in eligible_plan_ids
|
|
351
387
|
}))
|
|
388
|
+
selection_sql: str | None
|
|
389
|
+
selection_parameters: tuple[Any, ...]
|
|
390
|
+
if eligible_plan_ids is not None and not eligible_plan_ids:
|
|
391
|
+
selection_sql = None
|
|
392
|
+
selection_parameters = ()
|
|
393
|
+
elif eligible_plan_ids is None:
|
|
394
|
+
selection_sql = """
|
|
395
|
+
SELECT * FROM localization_jobs
|
|
396
|
+
WHERE status IN ('pending', 'retry_wait')
|
|
397
|
+
AND next_attempt_at <= ? AND attempts < max_attempts
|
|
398
|
+
ORDER BY created_at, target_locale, job_id
|
|
399
|
+
LIMIT 1
|
|
400
|
+
"""
|
|
401
|
+
selection_parameters = (now,)
|
|
402
|
+
else:
|
|
403
|
+
placeholders = ",".join("?" for _ in eligible_plan_ids)
|
|
404
|
+
selection_sql = f"""
|
|
405
|
+
SELECT jobs.* FROM localization_jobs AS jobs
|
|
406
|
+
WHERE jobs.status IN ('pending', 'retry_wait')
|
|
407
|
+
AND jobs.next_attempt_at <= ? AND jobs.attempts < jobs.max_attempts
|
|
408
|
+
AND EXISTS (
|
|
409
|
+
SELECT 1 FROM localization_plan_jobs AS mapping
|
|
410
|
+
WHERE mapping.job_id = jobs.job_id
|
|
411
|
+
AND mapping.plan_id IN ({placeholders})
|
|
412
|
+
)
|
|
413
|
+
ORDER BY jobs.created_at, jobs.target_locale, jobs.job_id
|
|
414
|
+
LIMIT 1
|
|
415
|
+
"""
|
|
416
|
+
selection_parameters = (now, *eligible_plan_ids)
|
|
352
417
|
blocked_error: str | None = None
|
|
353
418
|
claimed: ClaimedJob | None = None
|
|
419
|
+
quarantined = 0
|
|
354
420
|
with _transaction(self.connection):
|
|
355
421
|
self.connection.execute("""
|
|
356
422
|
UPDATE localization_jobs
|
|
@@ -368,31 +434,12 @@ class LocalizationQueue:
|
|
|
368
434
|
WHERE status = 'leased' AND lease_expires_at <= ?
|
|
369
435
|
AND attempts < max_attempts
|
|
370
436
|
""", (now, now, now))
|
|
371
|
-
|
|
372
|
-
row =
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
AND next_attempt_at <= ? AND attempts < max_attempts
|
|
378
|
-
ORDER BY created_at, target_locale, job_id
|
|
379
|
-
LIMIT 1
|
|
380
|
-
""", (now,)).fetchone()
|
|
381
|
-
else:
|
|
382
|
-
placeholders = ",".join("?" for _ in eligible_plan_ids)
|
|
383
|
-
row = self.connection.execute(f"""
|
|
384
|
-
SELECT jobs.* FROM localization_jobs AS jobs
|
|
385
|
-
WHERE jobs.status IN ('pending', 'retry_wait')
|
|
386
|
-
AND jobs.next_attempt_at <= ? AND jobs.attempts < jobs.max_attempts
|
|
387
|
-
AND EXISTS (
|
|
388
|
-
SELECT 1 FROM localization_plan_jobs AS mapping
|
|
389
|
-
WHERE mapping.job_id = jobs.job_id
|
|
390
|
-
AND mapping.plan_id IN ({placeholders})
|
|
391
|
-
)
|
|
392
|
-
ORDER BY jobs.created_at, jobs.target_locale, jobs.job_id
|
|
393
|
-
LIMIT 1
|
|
394
|
-
""", (now, *eligible_plan_ids)).fetchone()
|
|
395
|
-
if row is not None:
|
|
437
|
+
while selection_sql is not None:
|
|
438
|
+
row = self.connection.execute(
|
|
439
|
+
selection_sql, selection_parameters,
|
|
440
|
+
).fetchone()
|
|
441
|
+
if row is None:
|
|
442
|
+
break
|
|
396
443
|
payload_hash = _hash_text(row["payload_json"])
|
|
397
444
|
if payload_hash != row["payload_sha256"]:
|
|
398
445
|
self.connection.execute("""
|
|
@@ -414,36 +461,59 @@ class LocalizationQueue:
|
|
|
414
461
|
""", (now, row["job_id"]))
|
|
415
462
|
blocked_error = "queued payload is no longer valid JSON"
|
|
416
463
|
else:
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
row["job_id"]
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
464
|
+
payload_before = _canonical_json(payload)
|
|
465
|
+
binding_valid = binding_validator(payload)
|
|
466
|
+
if _canonical_json(payload) != payload_before:
|
|
467
|
+
raise LocalizationQueueBlocked(
|
|
468
|
+
"binding_validator must not mutate queued payload"
|
|
469
|
+
)
|
|
470
|
+
if binding_valid is not True:
|
|
471
|
+
self.connection.execute("""
|
|
472
|
+
UPDATE localization_jobs
|
|
473
|
+
SET status = 'failed',
|
|
474
|
+
last_error_code = 'job_binding_invalid',
|
|
475
|
+
last_error_detail_hash = NULL,
|
|
476
|
+
updated_at = ?
|
|
477
|
+
WHERE job_id = ?
|
|
478
|
+
""", (now, row["job_id"]))
|
|
479
|
+
blocked_error = "queued job binding is no longer current"
|
|
480
|
+
quarantined += 1
|
|
481
|
+
if quarantined >= MAX_BINDING_QUARANTINE:
|
|
482
|
+
break
|
|
483
|
+
continue
|
|
484
|
+
else:
|
|
485
|
+
lease_token = secrets.token_urlsafe(32)
|
|
486
|
+
lease_expires_at = now + lease_seconds
|
|
487
|
+
updated = self.connection.execute("""
|
|
488
|
+
UPDATE localization_jobs
|
|
489
|
+
SET status = 'leased', attempts = attempts + 1,
|
|
490
|
+
lease_owner = ?, lease_token = ?, lease_expires_at = ?,
|
|
491
|
+
last_error_code = NULL, last_error_detail_hash = NULL,
|
|
492
|
+
updated_at = ?
|
|
493
|
+
WHERE job_id = ? AND status IN ('pending', 'retry_wait')
|
|
494
|
+
""", (
|
|
495
|
+
worker_id,
|
|
496
|
+
lease_token,
|
|
497
|
+
lease_expires_at,
|
|
498
|
+
now,
|
|
499
|
+
row["job_id"],
|
|
500
|
+
))
|
|
501
|
+
if updated.rowcount != 1:
|
|
502
|
+
raise LocalizationQueueBlocked("claim lost its transactional job identity")
|
|
503
|
+
claimed = ClaimedJob(
|
|
504
|
+
job_id=row["job_id"],
|
|
505
|
+
plan_ids=self._plan_ids(row["job_id"]),
|
|
506
|
+
target_locale=row["target_locale"],
|
|
507
|
+
payload=payload,
|
|
508
|
+
attempt=int(row["attempts"]) + 1,
|
|
509
|
+
max_attempts=int(row["max_attempts"]),
|
|
510
|
+
lease_owner=worker_id,
|
|
511
|
+
lease_token=lease_token,
|
|
512
|
+
lease_expires_at=lease_expires_at,
|
|
513
|
+
)
|
|
514
|
+
break
|
|
515
|
+
break
|
|
516
|
+
if blocked_error and claimed is None:
|
|
447
517
|
raise LocalizationQueueBlocked(blocked_error)
|
|
448
518
|
return claimed
|
|
449
519
|
|