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.
Files changed (106) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +17 -1
  3. package/agent-spine-plugin/CHANGELOG.md +1 -1
  4. package/agent-spine-plugin/docs/host-integration.md +1 -15
  5. package/agent-spine-plugin/docs/preflight-recall.md +1 -1
  6. package/agent-spine-plugin/scripts/check-install.js +1 -6
  7. package/agent-spine-plugin/scripts/hermetic-process.js +8 -0
  8. package/agent-spine-plugin/scripts/release-check.js +2 -3
  9. package/agent-spine-plugin/scripts/run-tests-hermetic.js +8 -11
  10. package/agent-spine-plugin/src/hook.js +40 -40
  11. package/agent-spine-plugin/src/lib/briefing.js +6 -3
  12. package/agent-spine-plugin/src/lib/hook-context.js +63 -64
  13. package/agent-spine-plugin/src/lib/hook-output.js +115 -94
  14. package/agent-spine-plugin/src/lib/hook-timeline.js +20 -11
  15. package/agent-spine-plugin/src/lib/host-instruction-budget.js +8 -14
  16. package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +0 -7
  17. package/agent-spine-plugin/src/lib/pre-answer-timeline-recall.js +104 -0
  18. package/agent-spine-plugin/src/lib/session-timeline-codex.js +0 -4
  19. package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +0 -3
  20. package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +0 -4
  21. package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +0 -2
  22. package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +0 -22
  23. package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +0 -4
  24. package/agent-spine-plugin/src/lib/session-timeline-prior.js +42 -50
  25. package/agent-spine-plugin/src/lib/session-timeline-provider.js +0 -3
  26. package/agent-spine-plugin/src/lib/session-timeline-query.js +0 -1
  27. package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +0 -4
  28. package/agent-spine-plugin/src/lib/session-timeline-source.js +0 -3
  29. package/agent-spine-plugin/src/lib/session-timeline-transport.js +0 -8
  30. package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +0 -3
  31. package/agent-spine-plugin/src/lib/session-timeline.js +370 -408
  32. package/agent-spine-plugin/src/lib/source-roots.js +26 -16
  33. package/agent-spine-plugin/src/lib/timeline-user-feedback.js +0 -1
  34. package/bin/core-bootstrap.js +18 -10
  35. package/bin/launcher-mode.js +5 -0
  36. package/bin/launcher-runtime.js +27 -0
  37. package/bin/verify-bundled-agent-sources.cjs +28 -2
  38. package/blun.mjs +798 -143
  39. package/bundled-agent-sources.json +192 -155
  40. package/package.json +1 -1
  41. package/standard-skills/translate-native/README.md +1334 -1
  42. package/standard-skills/translate-native/SKILL.md +1 -1
  43. package/standard-skills/translate-native/VERSION +1 -1
  44. package/standard-skills/translate-native/docs/PREMORTEM.md +901 -0
  45. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +208 -41
  46. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1319 -77
  47. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +66 -11
  48. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +19 -1
  49. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +24 -6
  50. package/standard-skills/translate-native/integrations/commercial_localization_profile.py +23 -0
  51. package/standard-skills/translate-native/integrations/website_localization.py +234 -10
  52. package/standard-skills/translate-native/integrations/website_localization_benchmark.py +39 -3
  53. package/standard-skills/translate-native/integrations/website_localization_cms.py +512 -10
  54. package/standard-skills/translate-native/integrations/website_localization_cms_client.py +116 -0
  55. package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +101 -11
  56. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +12 -1
  57. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +186 -24
  58. package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +64 -1
  59. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +528 -5
  60. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth.py +918 -0
  61. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_auth_runtime.py +609 -0
  62. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_client.py +885 -0
  63. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_http.py +1244 -0
  64. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +220 -0
  65. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_sidecar_adapter.py +331 -0
  66. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_capabilities_http.py +444 -0
  67. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_client.py +673 -0
  68. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch.py +1317 -0
  69. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_client.py +826 -0
  70. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_http.py +1152 -0
  71. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_openapi.py +896 -0
  72. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_dispatch_runtime.py +621 -0
  73. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_http.py +1180 -0
  74. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_submission_runtime.py +1479 -0
  75. package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +133 -17
  76. package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +319 -4
  77. package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +72 -2
  78. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +13 -0
  79. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +56 -13
  80. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_openapi.py +335 -0
  81. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +66 -1
  82. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +73 -7
  83. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +93 -9
  84. package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +221 -11
  85. package/standard-skills/translate-native/integrations/website_localization_health.py +6 -0
  86. package/standard-skills/translate-native/integrations/website_localization_http_provider.py +1 -1
  87. package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +350 -0
  88. package/standard-skills/translate-native/integrations/website_localization_queue.py +126 -56
  89. package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +166 -13
  90. package/standard-skills/translate-native/integrations/website_localization_release.py +423 -17
  91. package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +168 -13
  92. package/standard-skills/translate-native/integrations/website_localization_runner.py +10 -0
  93. package/standard-skills/translate-native/integrations/website_localization_worker.py +213 -9
  94. package/standard-skills/translate-native/provenance.json +2 -2
  95. package/standard-skills/translate-native/references/commercial-localization.md +96 -17
  96. package/standard-skills/translate-native/scripts/blun_language_guard.py +3 -1
  97. package/standard-skills/translate-native/scripts/check_commercial_review.py +69 -9
  98. package/standard-skills/translate-native/scripts/commercial_localization_profile.py +829 -31
  99. package/standard-tools/language-guard/VERSION +1 -1
  100. package/standard-tools/language-guard/blun_language_guard.py +3 -1
  101. package/standard-tools/language-guard/check_commercial_review.py +69 -9
  102. package/standard-tools/language-guard/commercial_localization_profile.py +829 -31
  103. package/standard-tools/language-guard/provenance.json +2 -2
  104. package/standard-tools/manifest.json +1 -1
  105. package/worker-host.mjs +215 -82
  106. package/agent-spine-plugin/scripts/check-install-king.js +0 -37
@@ -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 typing import Any, Iterator, Mapping
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
- if eligible_plan_ids is not None and not eligible_plan_ids:
372
- row = None
373
- elif eligible_plan_ids is None:
374
- row = self.connection.execute("""
375
- SELECT * FROM localization_jobs
376
- WHERE status IN ('pending', 'retry_wait')
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
- lease_token = secrets.token_urlsafe(32)
418
- lease_expires_at = now + lease_seconds
419
- updated = self.connection.execute("""
420
- UPDATE localization_jobs
421
- SET status = 'leased', attempts = attempts + 1,
422
- lease_owner = ?, lease_token = ?, lease_expires_at = ?,
423
- last_error_code = NULL, last_error_detail_hash = NULL,
424
- updated_at = ?
425
- WHERE job_id = ? AND status IN ('pending', 'retry_wait')
426
- """, (
427
- worker_id,
428
- lease_token,
429
- lease_expires_at,
430
- now,
431
- row["job_id"],
432
- ))
433
- if updated.rowcount != 1:
434
- raise LocalizationQueueBlocked("claim lost its transactional job identity")
435
- claimed = ClaimedJob(
436
- job_id=row["job_id"],
437
- plan_ids=self._plan_ids(row["job_id"]),
438
- target_locale=row["target_locale"],
439
- payload=payload,
440
- attempt=int(row["attempts"]) + 1,
441
- max_attempts=int(row["max_attempts"]),
442
- lease_owner=worker_id,
443
- lease_token=lease_token,
444
- lease_expires_at=lease_expires_at,
445
- )
446
- if blocked_error:
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