blun-king-cli 9.1.587 → 9.1.588

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 (436) hide show
  1. package/CHANGELOG.md +11 -185
  2. package/LIESMICH.txt +51 -13
  3. package/README.md +44 -47
  4. package/agent-spine-plugin/.codex-plugin/plugin.json +16 -4
  5. package/agent-spine-plugin/CHANGELOG.md +37 -5
  6. package/agent-spine-plugin/README.md +3 -3
  7. package/agent-spine-plugin/blun.plugin.json +45 -10
  8. package/agent-spine-plugin/docs/artifact-evaluation.md +93 -0
  9. package/agent-spine-plugin/docs/host-integration.md +42 -27
  10. package/agent-spine-plugin/docs/preflight-recall.md +4 -2
  11. package/agent-spine-plugin/docs/session-timeline.md +97 -236
  12. package/agent-spine-plugin/docs/world-model.md +25 -0
  13. package/agent-spine-plugin/hooks/codex.json +1 -1
  14. package/agent-spine-plugin/hooks/hooks.json +1 -1
  15. package/agent-spine-plugin/package.json +1 -3
  16. package/agent-spine-plugin/scripts/check-hosts.js +3 -3
  17. package/agent-spine-plugin/scripts/release-check.js +10 -5
  18. package/agent-spine-plugin/scripts/run-checks.js +4 -1
  19. package/agent-spine-plugin/scripts/run-tests-hermetic.js +32 -6
  20. package/agent-spine-plugin/src/cli-learning.js +15 -0
  21. package/agent-spine-plugin/src/cli.js +2 -0
  22. package/agent-spine-plugin/src/hook.js +32 -32
  23. package/agent-spine-plugin/src/lib/action-lesson-recall.js +73 -8
  24. package/agent-spine-plugin/src/lib/briefing.js +146 -36
  25. package/agent-spine-plugin/src/lib/channel-continuity.js +19 -0
  26. package/agent-spine-plugin/src/lib/delivery-agent-usage.js +14 -7
  27. package/agent-spine-plugin/src/lib/gateway-group-response.js +128 -0
  28. package/agent-spine-plugin/src/lib/gateway-runs.js +24 -15
  29. package/agent-spine-plugin/src/lib/hook-briefing-use.js +13 -3
  30. package/agent-spine-plugin/src/lib/hook-context.js +16 -3
  31. package/agent-spine-plugin/src/lib/hook-output.js +129 -5
  32. package/agent-spine-plugin/src/lib/hook-timeline.js +5 -3
  33. package/agent-spine-plugin/src/lib/indexed-memory.js +2 -2
  34. package/agent-spine-plugin/src/lib/learning-artifact-evaluator.js +114 -0
  35. package/agent-spine-plugin/src/lib/learning-context.js +11 -4
  36. package/agent-spine-plugin/src/lib/learning-measurements.js +2 -2
  37. package/agent-spine-plugin/src/lib/mcp-runtime.js +89 -3
  38. package/agent-spine-plugin/src/lib/mcp-source-context.js +12 -2
  39. package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +91 -8
  40. package/agent-spine-plugin/src/lib/mcp-world-tools.js +2 -2
  41. package/agent-spine-plugin/src/lib/owned-file-lock.js +20 -1
  42. package/agent-spine-plugin/src/lib/persona-runtime.js +2 -2
  43. package/agent-spine-plugin/src/lib/preflight-delivery-id.js +27 -0
  44. package/agent-spine-plugin/src/lib/preflight.js +4 -4
  45. package/agent-spine-plugin/src/lib/session-timeline-codex.js +15 -0
  46. package/agent-spine-plugin/src/lib/session-timeline-contract.js +12 -4
  47. package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +36 -7
  48. package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +13 -10
  49. package/agent-spine-plugin/src/lib/session-timeline-invocation.js +1 -1
  50. package/agent-spine-plugin/src/lib/session-timeline-king.js +14 -0
  51. package/agent-spine-plugin/src/lib/session-timeline-prior.js +18 -12
  52. package/agent-spine-plugin/src/lib/session-timeline-provider.js +5 -0
  53. package/agent-spine-plugin/src/lib/session-timeline-query.js +2 -0
  54. package/agent-spine-plugin/src/lib/session-timeline-results.js +35 -10
  55. package/agent-spine-plugin/src/lib/session-timeline-source-open.js +30 -0
  56. package/agent-spine-plugin/src/lib/session-timeline.js +122 -75
  57. package/agent-spine-plugin/src/lib/source-roots.js +3 -2
  58. package/agent-spine-plugin/src/lib/task-knowledge-context.js +22 -1
  59. package/agent-spine-plugin/src/lib/timeline-continuation-update.js +100 -0
  60. package/agent-spine-plugin/src/lib/timeline-tool-guard.js +30 -7
  61. package/agent-spine-plugin/src/lib/timeline-user-feedback.js +217 -0
  62. package/agent-spine-plugin/src/lib/timeline-world-capture.js +233 -0
  63. package/agent-spine-plugin/src/lib/world-knowledge.js +59 -2
  64. package/agent-spine-plugin/src/lib/world-model.js +64 -9
  65. package/agent-spine-plugin/src/worker.js +13 -1
  66. package/bin/blun.js +43 -28
  67. package/bin/core-bootstrap.js +5 -4
  68. package/bin/king.js +43 -28
  69. package/bin/launcher-mode.js +1 -10
  70. package/bin/launcher-runtime.js +128 -295
  71. package/bin/managed-node.js +0 -0
  72. package/bin/managed-plugin-selection.cjs +0 -1
  73. package/bin/native-module-repair.js +0 -0
  74. package/bin/node-runtime.js +0 -0
  75. package/bin/node-version.js +0 -0
  76. package/bin/plugin-bootstrap.js +56 -120
  77. package/bin/private-paths.js +11 -34
  78. package/bin/standard-tools-bootstrap.js +34 -114
  79. package/bin/turn-thinking-policy.cjs +3 -11
  80. package/bin/update-copy.js +200 -0
  81. package/bin/update-lease.js +0 -0
  82. package/bin/update-notice.js +136 -289
  83. package/bin/verify-agent-behavior.cjs +122 -0
  84. package/bin/verify-agent-components.cjs +104 -0
  85. package/bin/verify-bundled-agent-sources.cjs +57 -0
  86. package/blun.mjs +143076 -135288
  87. package/bundled-agent-sources.json +701 -0
  88. package/package.json +12 -15
  89. package/standard-skills/translate-native/README.md +1293 -0
  90. package/standard-skills/translate-native/SKILL.md +172 -22
  91. package/standard-skills/translate-native/VERSION +1 -1
  92. package/standard-skills/translate-native/agents/openai.yaml +18 -0
  93. package/standard-skills/translate-native/assets/icon.svg +8 -0
  94. package/standard-skills/translate-native/docs/BLUN_CODE_INTEGRATION.md +76 -0
  95. package/standard-skills/translate-native/docs/PREMORTEM.md +489 -0
  96. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION.md +2035 -0
  97. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_API.md +1302 -0
  98. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_EVIDENCE_HTTP.md +136 -0
  99. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HEALTH_HTTP.md +130 -0
  100. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_HTTP_PROVIDER.md +175 -0
  101. package/standard-skills/translate-native/docs/WEBSITE_LOCALIZATION_RECEIPT_VERIFIER_HTTP.md +86 -0
  102. package/standard-skills/translate-native/integrations/AGENT_RULES.md +32 -0
  103. package/standard-skills/translate-native/integrations/adapters/blun-code-language-guard.js +514 -0
  104. package/standard-skills/translate-native/integrations/adapters/node-language-guard.js +230 -0
  105. package/standard-skills/translate-native/integrations/audit_log.py +327 -0
  106. package/standard-skills/translate-native/integrations/claude_language_hook.js +1536 -0
  107. package/standard-skills/translate-native/integrations/commercial_localization_profile.py +42 -0
  108. package/standard-skills/translate-native/integrations/delivery-policy.example.json +28 -0
  109. package/standard-skills/translate-native/integrations/enforced_delivery.py +543 -0
  110. package/standard-skills/translate-native/integrations/guard_service.py +435 -0
  111. package/standard-skills/translate-native/integrations/language_gateway.py +67 -0
  112. package/standard-skills/translate-native/integrations/mcp_auth_headers.py +198 -0
  113. package/standard-skills/translate-native/integrations/mcp_http_gateway.py +429 -0
  114. package/standard-skills/translate-native/integrations/non_language_html_entities.js +1485 -0
  115. package/standard-skills/translate-native/integrations/pre_output_guard.py +65 -0
  116. package/standard-skills/translate-native/integrations/task_router.py +101 -0
  117. package/standard-skills/translate-native/integrations/website_localization.py +401 -0
  118. package/standard-skills/translate-native/integrations/website_localization_api.py +581 -0
  119. package/standard-skills/translate-native/integrations/website_localization_benchmark.py +1885 -0
  120. package/standard-skills/translate-native/integrations/website_localization_benchmark_campaign.py +1772 -0
  121. package/standard-skills/translate-native/integrations/website_localization_benchmark_candidate.py +506 -0
  122. package/standard-skills/translate-native/integrations/website_localization_benchmark_http.py +400 -0
  123. package/standard-skills/translate-native/integrations/website_localization_benchmark_review_store.py +781 -0
  124. package/standard-skills/translate-native/integrations/website_localization_benchmark_reviewer_http.py +500 -0
  125. package/standard-skills/translate-native/integrations/website_localization_benchmark_runtime.py +1107 -0
  126. package/standard-skills/translate-native/integrations/website_localization_benchmark_suite.py +463 -0
  127. package/standard-skills/translate-native/integrations/website_localization_cms.py +2835 -0
  128. package/standard-skills/translate-native/integrations/website_localization_cms_client.py +875 -0
  129. package/standard-skills/translate-native/integrations/website_localization_cms_dispatch.py +805 -0
  130. package/standard-skills/translate-native/integrations/website_localization_cms_http.py +588 -0
  131. package/standard-skills/translate-native/integrations/website_localization_cms_lifecycle_monitor.py +991 -0
  132. package/standard-skills/translate-native/integrations/website_localization_cms_receiver.py +1441 -0
  133. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_runtime.py +414 -0
  134. package/standard-skills/translate-native/integrations/website_localization_cms_receiver_store.py +1073 -0
  135. package/standard-skills/translate-native/integrations/website_localization_cms_removal_dispatch.py +865 -0
  136. package/standard-skills/translate-native/integrations/website_localization_cms_source_client.py +583 -0
  137. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery.py +964 -0
  138. package/standard-skills/translate-native/integrations/website_localization_cms_source_delivery_runtime.py +665 -0
  139. package/standard-skills/translate-native/integrations/website_localization_cms_source_http.py +1153 -0
  140. package/standard-skills/translate-native/integrations/website_localization_cms_source_runtime.py +675 -0
  141. package/standard-skills/translate-native/integrations/website_localization_cms_source_service.py +1125 -0
  142. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification.py +674 -0
  143. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_http.py +444 -0
  144. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver.py +1469 -0
  145. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_notification_receiver_runtime.py +1142 -0
  146. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_processing_monitor.py +634 -0
  147. package/standard-skills/translate-native/integrations/website_localization_cms_terminal_receiver_client.py +804 -0
  148. package/standard-skills/translate-native/integrations/website_localization_deepl_baseline.py +922 -0
  149. package/standard-skills/translate-native/integrations/website_localization_evidence_http.py +482 -0
  150. package/standard-skills/translate-native/integrations/website_localization_health.py +1541 -0
  151. package/standard-skills/translate-native/integrations/website_localization_health_http.py +372 -0
  152. package/standard-skills/translate-native/integrations/website_localization_http_provider.py +297 -0
  153. package/standard-skills/translate-native/integrations/website_localization_native_reference_http.py +479 -0
  154. package/standard-skills/translate-native/integrations/website_localization_native_reference_intake.py +363 -0
  155. package/standard-skills/translate-native/integrations/website_localization_native_reference_queue.py +1449 -0
  156. package/standard-skills/translate-native/integrations/website_localization_native_reference_store.py +420 -0
  157. package/standard-skills/translate-native/integrations/website_localization_quality_profiles.py +235 -0
  158. package/standard-skills/translate-native/integrations/website_localization_queue.py +671 -0
  159. package/standard-skills/translate-native/integrations/website_localization_receipt_verifier_http.py +516 -0
  160. package/standard-skills/translate-native/integrations/website_localization_release.py +928 -0
  161. package/standard-skills/translate-native/integrations/website_localization_release_coordinator.py +1008 -0
  162. package/standard-skills/translate-native/integrations/website_localization_runner.py +276 -0
  163. package/standard-skills/translate-native/integrations/website_localization_runtime.py +862 -0
  164. package/standard-skills/translate-native/integrations/website_localization_service.py +350 -0
  165. package/standard-skills/translate-native/integrations/website_localization_supervisor.py +511 -0
  166. package/standard-skills/translate-native/integrations/website_localization_worker.py +663 -0
  167. package/standard-skills/translate-native/provenance.json +3 -4
  168. package/standard-skills/translate-native/references/commercial-localization.md +177 -0
  169. package/standard-skills/translate-native/scripts/blun_language_guard.py +7 -1
  170. package/standard-skills/translate-native/scripts/check_commercial_review.py +80 -0
  171. package/standard-skills/translate-native/scripts/commercial_localization_profile.py +333 -0
  172. package/standard-tools/language-guard/LICENSE +21 -0
  173. package/standard-tools/language-guard/VERSION +1 -0
  174. package/standard-tools/language-guard/blun_language_guard.py +7 -1
  175. package/standard-tools/language-guard/check_commercial_review.py +80 -0
  176. package/standard-tools/language-guard/commercial_localization_profile.py +333 -0
  177. package/standard-tools/language-guard/language_gateway.py +62 -0
  178. package/standard-tools/language-guard/pre_output_guard.py +64 -0
  179. package/standard-tools/language-guard/provenance.json +4 -11
  180. package/standard-tools/manifest.json +34 -11
  181. package/telegram-plugin/commands/access.md +2 -10
  182. package/telegram-plugin/dist/bridge.mjs +64041 -687
  183. package/telegram-plugin/dist/mcp-server.mjs +72810 -9027
  184. package/telegram-plugin/dist/noise.mjs +28 -63511
  185. package/agent-spine-plugin/CONTRIBUTING.md +0 -52
  186. package/agent-spine-plugin/SECURITY.md +0 -47
  187. package/agent-spine-plugin/docs/assignment-continuation.md +0 -48
  188. package/agent-spine-plugin/docs/releasing.md +0 -85
  189. package/agent-spine-plugin/docs/structured-completion.md +0 -67
  190. package/bin/abort-listener-policy.cjs +0 -43
  191. package/bin/active-steer-priority-policy.cjs +0 -24
  192. package/bin/agent-api-http-adapter.mjs +0 -446
  193. package/bin/agent-api-private-http-server.mjs +0 -288
  194. package/bin/agent-api-runtime.mjs +0 -252
  195. package/bin/agent-api-service-environment.mjs +0 -236
  196. package/bin/agent-api-service-host.mjs +0 -209
  197. package/bin/agent-api-service-process.mjs +0 -171
  198. package/bin/agent-api-session-registry.mjs +0 -428
  199. package/bin/agent-api-tool-broker.cjs +0 -248
  200. package/bin/agent-api-turn-controller.mjs +0 -461
  201. package/bin/agent-api-usage-journal.cjs +0 -259
  202. package/bin/agent-resume-snapshot.cjs +0 -241
  203. package/bin/agentspine-king-goal-inbox.mjs +0 -111
  204. package/bin/agentspine-king-goal-intake.mjs +0 -106
  205. package/bin/approval-rejection-stop.cjs +0 -15
  206. package/bin/assistant-message-offload-policy.cjs +0 -284
  207. package/bin/baseline-skill-performance-policy.cjs +0 -39
  208. package/bin/bash-search-scope-policy.cjs +0 -49
  209. package/bin/codebase-search-runtime.cjs +0 -23
  210. package/bin/cognitive-action-checkpoint.cjs +0 -1104
  211. package/bin/cognitive-attention-delivery.cjs +0 -76
  212. package/bin/cognitive-attention-policy.cjs +0 -143
  213. package/bin/cognitive-attention-runtime.cjs +0 -91
  214. package/bin/cognitive-context-projection.cjs +0 -73
  215. package/bin/cognitive-cross-portal-acceptance.cjs +0 -443
  216. package/bin/cognitive-effective-view.cjs +0 -77
  217. package/bin/cognitive-focus-projection.cjs +0 -206
  218. package/bin/cognitive-focus-scope.cjs +0 -37
  219. package/bin/cognitive-goal-autostart-policy.cjs +0 -72
  220. package/bin/cognitive-goal-time-trigger-controller.cjs +0 -146
  221. package/bin/cognitive-memory-adapter.cjs +0 -282
  222. package/bin/cognitive-memory-command.cjs +0 -293
  223. package/bin/cognitive-memory-provider.cjs +0 -92
  224. package/bin/cognitive-salience-policy.cjs +0 -159
  225. package/bin/cognitive-state-store.cjs +0 -508
  226. package/bin/cognitive-turn-lifecycle.cjs +0 -624
  227. package/bin/cognitive-work-focus.cjs +0 -180
  228. package/bin/compaction-history-archive.cjs +0 -166
  229. package/bin/compaction-history-startup.cjs +0 -50
  230. package/bin/compaction-model-policy.cjs +0 -31
  231. package/bin/compaction-stage-policy.cjs +0 -21
  232. package/bin/compaction-transaction-policy.cjs +0 -122
  233. package/bin/config-write-dedup-policy.cjs +0 -27
  234. package/bin/context-budget-ledger.cjs +0 -31
  235. package/bin/context-doctor-policy.cjs +0 -70
  236. package/bin/context-insight-policy.cjs +0 -36
  237. package/bin/context-performance-policy.cjs +0 -19
  238. package/bin/context-pressure-policy.cjs +0 -20
  239. package/bin/cron-run-output.cjs +0 -45
  240. package/bin/cron-run-store.cjs +0 -145
  241. package/bin/curiosity-scout-policy.cjs +0 -49
  242. package/bin/default-model-output-budget-policy.cjs +0 -28
  243. package/bin/durable-task-resume-policy.cjs +0 -130
  244. package/bin/durable-task-resume-runtime.cjs +0 -117
  245. package/bin/durable-task-resume-store.cjs +0 -88
  246. package/bin/editable-tool-approval-policy.cjs +0 -540
  247. package/bin/editable-tool-approval-runtime.cjs +0 -99
  248. package/bin/effective-system-prompt-cache-policy.cjs +0 -33
  249. package/bin/error-memory-performance-policy.cjs +0 -113
  250. package/bin/file-observation-policy.cjs +0 -133
  251. package/bin/foreground-output-capture-policy.cjs +0 -41
  252. package/bin/generated-source-health.cjs +0 -142
  253. package/bin/glob-pattern-policy.cjs +0 -13
  254. package/bin/goal-completion-evidence-policy.cjs +0 -120
  255. package/bin/grep-output-limit-policy.cjs +0 -39
  256. package/bin/historical-media-projection-policy.cjs +0 -48
  257. package/bin/history-offload-pressure-policy.cjs +0 -33
  258. package/bin/html-to-research-markdown.cjs +0 -147
  259. package/bin/identity-context-policy.cjs +0 -764
  260. package/bin/identity-journal-policy.cjs +0 -107
  261. package/bin/input-draft-persistence.cjs +0 -77
  262. package/bin/king-tui-function-contract.json +0 -33
  263. package/bin/launcher-restart-policy.cjs +0 -150
  264. package/bin/live-response-repetition-guard.cjs +0 -196
  265. package/bin/llm-config-log-dedup-policy.cjs +0 -76
  266. package/bin/loop-event-record-policy.cjs +0 -174
  267. package/bin/managed-context-startup-policy.cjs +0 -27
  268. package/bin/media-activity-layout-policy.cjs +0 -34
  269. package/bin/media-auto-retrieval-policy.cjs +0 -90
  270. package/bin/media-result-policy.cjs +0 -59
  271. package/bin/micro-compaction-policy.cjs +0 -145
  272. package/bin/mistake-relevance-policy.cjs +0 -319
  273. package/bin/model-retry-progress-policy.cjs +0 -46
  274. package/bin/native-large-file-io.cjs +0 -42
  275. package/bin/native-runtime-cache.cjs +0 -76
  276. package/bin/natural-presence-policy.cjs +0 -28
  277. package/bin/noninteractive-shell-env-policy.cjs +0 -19
  278. package/bin/observer-hooks.cjs +0 -14
  279. package/bin/outbound-claim-provenance.cjs +0 -150
  280. package/bin/oversized-context-offload-policy.cjs +0 -86
  281. package/bin/pending-media-policy.cjs +0 -182
  282. package/bin/pending-token-estimate-policy.cjs +0 -41
  283. package/bin/personal-memory-consent-policy.cjs +0 -72
  284. package/bin/personal-memory-performance-policy.cjs +0 -12
  285. package/bin/personality-choice-policy.cjs +0 -101
  286. package/bin/personality-memory-adapter.cjs +0 -379
  287. package/bin/personality-mode.cjs +0 -46
  288. package/bin/personality-setup-policy.cjs +0 -197
  289. package/bin/proactive-compaction-policy.cjs +0 -25
  290. package/bin/profile-identity-resolution.cjs +0 -136
  291. package/bin/profile-runtime.cjs +0 -318
  292. package/bin/profile-tool-exclusion-policy.cjs +0 -37
  293. package/bin/programmatic-context-isolation.cjs +0 -25
  294. package/bin/programmatic-tool-runtime.mjs +0 -627
  295. package/bin/provider-idle-timeout-policy.cjs +0 -14
  296. package/bin/provider-model-refresh-deadline.cjs +0 -53
  297. package/bin/provider-model-refresh-policy.cjs +0 -107
  298. package/bin/rate-limit-recovery-policy.cjs +0 -47
  299. package/bin/read-batch-policy.cjs +0 -32
  300. package/bin/read-continuation-policy.cjs +0 -59
  301. package/bin/recurring-cron-history-policy.cjs +0 -124
  302. package/bin/relationship-continuity-policy.cjs +0 -143
  303. package/bin/relationship-curiosity-policy.cjs +0 -107
  304. package/bin/relationship-learning-policy.cjs +0 -168
  305. package/bin/release-artifact-freeze-policy.cjs +0 -30
  306. package/bin/reload-plugin-bootstrap.cjs +0 -18
  307. package/bin/reload-queue-policy.cjs +0 -38
  308. package/bin/repeated-assistant-response-policy.cjs +0 -232
  309. package/bin/repeated-injection-projection.cjs +0 -107
  310. package/bin/repeated-user-message-projection.cjs +0 -8
  311. package/bin/research-page-result.cjs +0 -74
  312. package/bin/retry-checkpoint-policy.cjs +0 -13
  313. package/bin/runtime-exit-ledger.cjs +0 -144
  314. package/bin/scoped-cron-run-policy.cjs +0 -358
  315. package/bin/session-checkpoint-policy.cjs +0 -25
  316. package/bin/session-compaction-policy.cjs +0 -84
  317. package/bin/session-replay-policy.cjs +0 -20
  318. package/bin/session-replay-window-policy.cjs +0 -40
  319. package/bin/session-resume-checkpoint.cjs +0 -254
  320. package/bin/session-scrollback-archive.cjs +0 -229
  321. package/bin/skill-activation-performance-policy.cjs +0 -69
  322. package/bin/skill-listing-performance-policy.cjs +0 -92
  323. package/bin/soul-organization-policy.cjs +0 -78
  324. package/bin/soul-preservation-policy.cjs +0 -20
  325. package/bin/startup-preferences.cjs +0 -131
  326. package/bin/streaming-flush-performance-policy.cjs +0 -28
  327. package/bin/structured-agent-swarm-output.cjs +0 -325
  328. package/bin/structured-subagent-output.cjs +0 -252
  329. package/bin/subagent-context-fork-policy.cjs +0 -155
  330. package/bin/subagent-max-tokens-handoff-policy.cjs +0 -69
  331. package/bin/subagent-parent-responsiveness.cjs +0 -19
  332. package/bin/subagent-skill-policy.cjs +0 -206
  333. package/bin/subagent-timeout-policy.cjs +0 -182
  334. package/bin/subagent-tool-policy.cjs +0 -60
  335. package/bin/subagent-usage-rollup-policy.cjs +0 -29
  336. package/bin/system-prompt-context-policy.cjs +0 -124
  337. package/bin/system-prompt-token-cache-policy.cjs +0 -60
  338. package/bin/telegram-addressed-focus.cjs +0 -55
  339. package/bin/telegram-addressed-priority.cjs +0 -12
  340. package/bin/telegram-approval-relay.cjs +0 -290
  341. package/bin/telegram-bot-priority.cjs +0 -17
  342. package/bin/telegram-console-status-policy.cjs +0 -174
  343. package/bin/telegram-context-projection-policy.cjs +0 -141
  344. package/bin/telegram-delivery-lifecycle.cjs +0 -125
  345. package/bin/telegram-direct-focus-policy.cjs +0 -273
  346. package/bin/telegram-mcp-compatibility.cjs +0 -49
  347. package/bin/telegram-media-delivery-policy.cjs +0 -42
  348. package/bin/telegram-private-conversation-policy.cjs +0 -185
  349. package/bin/telegram-queue-handoff-policy.cjs +0 -73
  350. package/bin/telegram-remote-status-policy.cjs +0 -120
  351. package/bin/telegram-session-queue-runtime.mjs +0 -306
  352. package/bin/telegram-text-chunk-policy.cjs +0 -63
  353. package/bin/telegram-truncated-reply-policy.cjs +0 -37
  354. package/bin/telegram-urgent-policy.cjs +0 -45
  355. package/bin/telemetry-spool-policy.cjs +0 -57
  356. package/bin/thinking-activity-status-policy.cjs +0 -132
  357. package/bin/thinking-only-guard.cjs +0 -80
  358. package/bin/todo-list-turn-policy.cjs +0 -131
  359. package/bin/tool-call-loop-policy.cjs +0 -51
  360. package/bin/tool-file-persistence.cjs +0 -141
  361. package/bin/tool-result-offload-policy.cjs +0 -359
  362. package/bin/tool-result-offload-telemetry.cjs +0 -12
  363. package/bin/tool-schema-token-cache-policy.cjs +0 -41
  364. package/bin/tool-stream-preview-policy.cjs +0 -9
  365. package/bin/tui-functional-contract.cjs +0 -55
  366. package/bin/turn-tool-performance-policy.cjs +0 -486
  367. package/bin/usage-cache-efficiency-policy.cjs +0 -26
  368. package/bin/user-home-path-policy.cjs +0 -13
  369. package/bin/user-message-offload-policy.cjs +0 -103
  370. package/bin/user-prompt-hook-origin-policy.cjs +0 -34
  371. package/bin/user-tool-record-policy.cjs +0 -7
  372. package/bin/validated-learning-insight-policy.cjs +0 -58
  373. package/bin/validated-learning-outcome-trace.cjs +0 -107
  374. package/bin/validated-learning-performance-policy.cjs +0 -53
  375. package/bin/validated-learning-signal.cjs +0 -463
  376. package/bin/windows-bash-dialect-policy.cjs +0 -25
  377. package/bin/windows-node-crash-dump.cjs +0 -110
  378. package/bin/write-continuation-policy.cjs +0 -69
  379. package/codebase-index/README.md +0 -82
  380. package/codebase-index/codebase_index.py +0 -470
  381. package/standard-skills/agent-browser/SKILL.md +0 -19
  382. package/standard-skills/agent-browser/references/runtime.md +0 -8
  383. package/standard-skills/blun-session-inspector/SKILL.md +0 -41
  384. package/standard-skills/blun-session-inspector/scripts/inspect-session.cjs +0 -437
  385. package/standard-skills/design-taste-frontend/SKILL.md +0 -1206
  386. package/standard-skills/full-output-enforcement/SKILL.md +0 -49
  387. package/standard-skills/high-end-visual-design/SKILL.md +0 -98
  388. package/standard-skills/image-to-code/SKILL.md +0 -1228
  389. package/standard-skills/industrial-brutalist-ui/SKILL.md +0 -92
  390. package/standard-skills/minimalist-ui/SKILL.md +0 -85
  391. package/standard-skills/motion-design-taste/SKILL.md +0 -74
  392. package/standard-skills/playwright-testing/SKILL.md +0 -19
  393. package/standard-skills/playwright-testing/references/runtime.md +0 -7
  394. package/standard-skills/premortem/SKILL.md +0 -148
  395. package/standard-skills/redesign-existing-projects/SKILL.md +0 -178
  396. package/standard-skills/research-evidence/SKILL.md +0 -39
  397. package/standard-skills/research-evidence/references/evidence-format.md +0 -104
  398. package/standard-skills/research-evidence/scripts/evidence-collection.cjs +0 -260
  399. package/standard-skills/research-evidence/scripts/score-report.cjs +0 -130
  400. package/standard-skills/screenshot-lesen/SKILL.md +0 -52
  401. package/standard-skills/stitch-design-taste/DESIGN.md +0 -121
  402. package/standard-skills/stitch-design-taste/SKILL.md +0 -184
  403. package/standard-skills/telegram-channel/SKILL.md +0 -18
  404. package/standard-skills/telegram-channel/references/runtime.md +0 -7
  405. package/standard-skills/venture-flywheel/SKILL.md +0 -32
  406. package/standard-skills/venture-flywheel/identity/project-identity.cjs +0 -146
  407. package/standard-skills/venture-flywheel/policy/capability-engine.cjs +0 -114
  408. package/standard-skills/venture-flywheel/policy/repository-trust.cjs +0 -229
  409. package/standard-skills/venture-flywheel/references/BEISPIELE-phase0.md +0 -146
  410. package/standard-skills/venture-flywheel/references/CAPABILITY-MAP.md +0 -34
  411. package/standard-skills/venture-flywheel/references/SPEC-phase0-identity-trust.md +0 -77
  412. package/standard-skills/venture-flywheel/references/SPEC-phase0-state-events.md +0 -93
  413. package/standard-skills/venture-flywheel/schemas/capability-decision.schema.json +0 -13
  414. package/standard-skills/venture-flywheel/schemas/execution-event.schema.json +0 -44
  415. package/standard-skills/venture-flywheel/schemas/project-identity.schema.json +0 -32
  416. package/standard-skills/venture-flywheel/schemas/repository-trust.schema.json +0 -57
  417. package/standard-skills/venture-flywheel/schemas/run-transition.schema.json +0 -59
  418. package/standard-skills/venture-flywheel/state/execution-event.cjs +0 -191
  419. package/standard-skills/venture-flywheel/state/task-state-machine.cjs +0 -190
  420. package/standard-skills/web-lesen/SKILL.md +0 -73
  421. package/standard-skills/web-lesen/scripts/crawl_public.py +0 -379
  422. package/standard-skills/windows-mcp/SKILL.md +0 -19
  423. package/standard-skills/windows-mcp/references/runtime.md +0 -9
  424. package/telegram-plugin/DELIVERY.md +0 -36
  425. package/telegram-plugin/bin/telegram-approval-relay.cjs +0 -290
  426. package/telegram-plugin/bin/telegram-console-status-policy.cjs +0 -175
  427. package/telegram-plugin/bin/telegram-delivery-lifecycle.cjs +0 -125
  428. package/telegram-plugin/bin/telegram-direct-reply-policy.cjs +0 -48
  429. package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +0 -122
  430. package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +0 -186
  431. package/telegram-plugin/bin/telegram-remote-status-policy.cjs +0 -121
  432. package/telegram-plugin/bin/telegram-reply-parts.cjs +0 -149
  433. package/telegram-plugin/bin/telegram-text-chunk-policy.cjs +0 -63
  434. package/telegram-plugin/bin/telegram-typing-keepalive.cjs +0 -89
  435. package/telegram-plugin/compat/mcp-server-fa511cd1.mjs +0 -73825
  436. /package/{bin → scripts}/fix-node-pty-perms.js +0 -0
@@ -0,0 +1,928 @@
1
+ #!/usr/bin/env python3
2
+ """Signed, version-bound translation memory and publication-readiness gate.
3
+
4
+ The host owns the SQLite connection, quality-receipt verifier, and isolated
5
+ approval authority. This module never reads a signing key and never publishes.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import importlib.util
12
+ import json
13
+ import re
14
+ import sqlite3
15
+ import sys
16
+ import unicodedata
17
+ from contextlib import contextmanager
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import Any, Iterator, Protocol
21
+
22
+
23
+ SCHEMA_VERSION = 1
24
+ APPROVAL_SCHEMA = "blun.website-localization-approval.v3"
25
+ RECEIPT_BINDING_SCHEMA = "blun.localization-quality-receipt-binding.v2"
26
+ INDEPENDENT_MODEL_REVIEW_SCHEMA = "blun.independent-model-review.v1"
27
+ PUBLICATION_EVIDENCE_SCHEMA = "blun.website-localization-release-evidence.v1"
28
+ MAX_TEXT_BYTES = 2_000_000
29
+ MAX_RECEIPT_LENGTH = 16_384
30
+ MAX_TTL_SECONDS = 31_536_000.0
31
+ HEX64 = re.compile(r"^[0-9a-f]{64}$")
32
+ TOKEN = re.compile(r"^[A-Za-z0-9_.:-]{1,256}$")
33
+ _COLUMNS = (
34
+ "approval_id", "job_id", "target_locale", "target_sha256",
35
+ "result_json", "result_sha256", "approval_json", "approval_sha256",
36
+ "signature_algorithm", "key_id", "signature", "approved_at", "expires_at",
37
+ )
38
+
39
+
40
+ def _load_module(name: str, path: Path):
41
+ spec = importlib.util.spec_from_file_location(name, path)
42
+ if spec is None or spec.loader is None:
43
+ raise RuntimeError(f"cannot load required release dependency: {path.name}")
44
+ module = importlib.util.module_from_spec(spec)
45
+ sys.modules[spec.name] = module
46
+ spec.loader.exec_module(module)
47
+ return module
48
+
49
+
50
+ _ROOT = Path(__file__).resolve().parents[1]
51
+ _QUEUE = _load_module(
52
+ "blun_website_localization_release_queue",
53
+ _ROOT / "integrations" / "website_localization_queue.py",
54
+ )
55
+ _WORKER = _load_module(
56
+ "blun_website_localization_release_worker",
57
+ _ROOT / "integrations" / "website_localization_worker.py",
58
+ )
59
+
60
+
61
+ class LocalizationReleaseBlocked(RuntimeError):
62
+ """Stable, content-free release failure."""
63
+
64
+ def __init__(self, code: str, *, retryable: bool = False):
65
+ if not isinstance(code, str) or TOKEN.fullmatch(code) is None:
66
+ raise ValueError("release failure code is invalid")
67
+ if not isinstance(retryable, bool):
68
+ raise ValueError("release failure retryability must be boolean")
69
+ super().__init__(code)
70
+ self.code = code
71
+ self.retryable = retryable
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class ApprovalSignature:
76
+ algorithm: str
77
+ key_id: str
78
+ signature: str
79
+
80
+
81
+ class ApprovalAuthority(Protocol):
82
+ def sign(self, payload: bytes) -> ApprovalSignature: ...
83
+ def verify(self, payload: bytes, signature: ApprovalSignature) -> bool: ...
84
+
85
+
86
+ class QualityReceiptVerifier(Protocol):
87
+ def verify(
88
+ self,
89
+ *,
90
+ binding: dict[str, Any],
91
+ receipt: str,
92
+ ) -> bool: ...
93
+
94
+
95
+ class IndependentModelReviewVerifier(Protocol):
96
+ def verify(
97
+ self,
98
+ *,
99
+ binding: dict[str, Any],
100
+ receipt: str,
101
+ ) -> bool: ...
102
+
103
+
104
+ @dataclass(frozen=True)
105
+ class ApprovedLocalization:
106
+ approval_id: str
107
+ job_id: str
108
+ target_locale: str
109
+ target_sha256: str
110
+ candidate: str
111
+ release_evidence: dict[str, Any]
112
+ approved_at: float
113
+ expires_at: float
114
+
115
+
116
+ @dataclass(frozen=True)
117
+ class WebsiteReadiness:
118
+ plan_id: str
119
+ ready: bool
120
+ required_locales: tuple[str, ...]
121
+ approved_locales: tuple[str, ...]
122
+ blocked: tuple[tuple[str, str], ...]
123
+
124
+
125
+ def validate_publication_evidence(value: Any) -> dict[str, Any]:
126
+ """Validate the content-free release proof carried to a CMS."""
127
+
128
+ if not isinstance(value, dict) or set(value) != {
129
+ "schema", "job_id", "target_locale", "target_sha256", "approval_id",
130
+ "content_type", "result_sha256", "approval_sha256",
131
+ "quality_receipt_sha256", "commercial_profile", "commercial_review",
132
+ }:
133
+ raise LocalizationReleaseBlocked("publication.evidence.invalid")
134
+ if (
135
+ value.get("schema") != PUBLICATION_EVIDENCE_SCHEMA
136
+ or any(
137
+ not isinstance(value.get(field), str)
138
+ or TOKEN.fullmatch(value[field]) is None
139
+ for field in ("job_id", "target_locale", "approval_id", "content_type")
140
+ )
141
+ or any(
142
+ HEX64.fullmatch(str(value.get(field))) is None
143
+ for field in (
144
+ "target_sha256", "result_sha256", "approval_sha256",
145
+ "quality_receipt_sha256",
146
+ )
147
+ )
148
+ ):
149
+ raise LocalizationReleaseBlocked("publication.evidence.invalid")
150
+ profile = value.get("commercial_profile")
151
+ review = value.get("commercial_review")
152
+ if (
153
+ (profile is None) != (review is None)
154
+ or (value["content_type"] == "commercial") != (profile is not None)
155
+ ):
156
+ raise LocalizationReleaseBlocked("publication.evidence.invalid")
157
+ if profile is not None:
158
+ if not isinstance(profile, str) or TOKEN.fullmatch(profile) is None:
159
+ raise LocalizationReleaseBlocked("publication.evidence.invalid")
160
+ try:
161
+ review = _WORKER._COMMERCIAL.validate_summary(
162
+ review,
163
+ profile,
164
+ review_required=(
165
+ isinstance(review, dict)
166
+ and review.get("status") == "review_required"
167
+ ),
168
+ )
169
+ except _WORKER._COMMERCIAL.CommercialReviewBlocked:
170
+ raise LocalizationReleaseBlocked("publication.evidence.invalid") from None
171
+ return json.loads(_canonical_json({**value, "commercial_review": review}))
172
+
173
+
174
+ @dataclass(frozen=True)
175
+ class VerifiedResultCache:
176
+ """Runner adapter for exact, signed translation-memory results."""
177
+
178
+ store: Any
179
+ authority: ApprovalAuthority
180
+
181
+ def resolve(
182
+ self,
183
+ job_payload: dict[str, Any],
184
+ *,
185
+ now: float,
186
+ ) -> dict[str, Any] | None:
187
+ return self.store.cached_result(job_payload, self.authority, now=now)
188
+
189
+
190
+ def _canonical_json(value: Any) -> str:
191
+ try:
192
+ return json.dumps(
193
+ value,
194
+ ensure_ascii=False,
195
+ allow_nan=False,
196
+ sort_keys=True,
197
+ separators=(",", ":"),
198
+ )
199
+ except (TypeError, ValueError) as error:
200
+ raise LocalizationReleaseBlocked("release.json.invalid") from error
201
+
202
+
203
+ def _hash_text(value: str) -> str:
204
+ return hashlib.sha256(value.encode("utf-8")).hexdigest()
205
+
206
+
207
+ def _text(value: Any, code: str, *, limit: int = 256) -> str:
208
+ if not isinstance(value, str) or not value or value != value.strip():
209
+ raise LocalizationReleaseBlocked(code)
210
+ if len(value) > limit or "\x00" in value or not unicodedata.is_normalized("NFC", value):
211
+ raise LocalizationReleaseBlocked(code)
212
+ return value
213
+
214
+
215
+ def _timestamp(value: Any, code: str) -> float:
216
+ if isinstance(value, bool) or not isinstance(value, (int, float)):
217
+ raise LocalizationReleaseBlocked(code)
218
+ value = float(value)
219
+ if value < 0 or value != value or value in {float("inf"), float("-inf")}:
220
+ raise LocalizationReleaseBlocked(code)
221
+ return value
222
+
223
+
224
+ def _receipt(value: Any, code: str) -> str:
225
+ return _text(value, code, limit=MAX_RECEIPT_LENGTH)
226
+
227
+
228
+ def _independent_model_review(value: Any, primary_provider: dict[str, Any]) -> tuple[dict[str, str], str]:
229
+ if not isinstance(value, dict) or set(value) != {"schema", "provider", "receipt"}:
230
+ raise LocalizationReleaseBlocked("independent_model_review.invalid")
231
+ provider = value.get("provider")
232
+ if not isinstance(provider, dict) or set(provider) != {"id", "model_id", "model_version"}:
233
+ raise LocalizationReleaseBlocked("independent_model_review.provider.invalid")
234
+ normalized = {
235
+ name: _text(provider.get(name), "independent_model_review.provider.invalid")
236
+ for name in ("id", "model_id", "model_version")
237
+ }
238
+ if value.get("schema") != INDEPENDENT_MODEL_REVIEW_SCHEMA:
239
+ raise LocalizationReleaseBlocked("independent_model_review.schema.invalid")
240
+ if normalized["id"] == primary_provider.get("id"):
241
+ raise LocalizationReleaseBlocked("independent_model_review.not_independent")
242
+ return normalized, _receipt(value.get("receipt"), "independent_model_review.receipt.required")
243
+
244
+
245
+ def _receipt_binding(
246
+ job_id: str,
247
+ job: dict[str, Any],
248
+ result: dict[str, Any],
249
+ result_sha256: str,
250
+ *,
251
+ review_kind: str,
252
+ review_provider: dict[str, str] | None = None,
253
+ ) -> dict[str, Any]:
254
+ """Build the exact canonical context that an evidence receipt must bind."""
255
+
256
+ if review_kind not in {"quality", "qualified_human", "independent_model"}:
257
+ raise LocalizationReleaseBlocked("review.binding.invalid")
258
+ if (review_kind == "independent_model") != (review_provider is not None):
259
+ raise LocalizationReleaseBlocked("review.binding.invalid")
260
+ binding = {
261
+ "schema": RECEIPT_BINDING_SCHEMA,
262
+ "review_kind": review_kind,
263
+ "job_id": job_id,
264
+ "result_sha256": result_sha256,
265
+ "source_text": job["source"]["text"],
266
+ "target_text": result["candidate"],
267
+ "source_sha256": result["source_sha256"],
268
+ "target_sha256": result["target_sha256"],
269
+ "source_locale": result["source_locale"],
270
+ "target_locale": result["target_locale"],
271
+ "content_type": result["content_type"],
272
+ "glossary_version": result["glossary_version"],
273
+ "policy_version": result["policy_version"],
274
+ "primary_provider": result["provider"],
275
+ "review_provider": review_provider,
276
+ "software_version": result["software_version"],
277
+ "review_confidence": result["review_confidence"],
278
+ "quality_profile": result["quality_profile"],
279
+ "commercial_profile": job.get("commercial_profile"),
280
+ "commercial_review": result["commercial_review"],
281
+ "human_review_required": result["human_review_required"],
282
+ "independent_review_required": result["independent_review_required"],
283
+ }
284
+ return json.loads(_canonical_json(binding))
285
+
286
+
287
+ def _verification_failure(error: Exception, verifier_kind: str) -> None:
288
+ if getattr(type(error), "localization_receipt_verification_failure", None) is not True:
289
+ return
290
+ code = getattr(error, "code", None)
291
+ retryable = getattr(error, "retryable", None)
292
+ combined = f"{verifier_kind}.verifier.{code}"
293
+ if (
294
+ isinstance(code, str)
295
+ and TOKEN.fullmatch(code) is not None
296
+ and isinstance(retryable, bool)
297
+ and len(combined) <= 256
298
+ ):
299
+ raise LocalizationReleaseBlocked(combined, retryable=retryable) from None
300
+
301
+
302
+ def _signature(value: Any) -> ApprovalSignature:
303
+ if not isinstance(value, ApprovalSignature):
304
+ raise LocalizationReleaseBlocked("approval.signature.invalid")
305
+ for item in (value.algorithm, value.key_id, value.signature):
306
+ if not isinstance(item, str) or TOKEN.fullmatch(item) is None:
307
+ raise LocalizationReleaseBlocked("approval.signature.invalid")
308
+ return value
309
+
310
+
311
+ @contextmanager
312
+ def _transaction(connection: sqlite3.Connection) -> Iterator[None]:
313
+ if connection.in_transaction:
314
+ raise LocalizationReleaseBlocked("release.transaction.external")
315
+ try:
316
+ connection.execute("BEGIN IMMEDIATE")
317
+ yield
318
+ except Exception:
319
+ connection.rollback()
320
+ raise
321
+ else:
322
+ connection.commit()
323
+
324
+
325
+ def _plan_job(plan: Any, job_id: str) -> tuple[str, dict[str, Any]]:
326
+ plan_id = _text(getattr(plan, "plan_id", None), "plan.invalid")
327
+ jobs = getattr(plan, "jobs", None)
328
+ if not isinstance(jobs, tuple) or not jobs:
329
+ raise LocalizationReleaseBlocked("plan.invalid")
330
+ matches = [job for job in jobs if getattr(job, "job_id", None) == job_id]
331
+ if len(matches) != 1 or not callable(getattr(matches[0], "as_payload", None)):
332
+ raise LocalizationReleaseBlocked("plan.job.missing")
333
+ payload = matches[0].as_payload()
334
+ if not isinstance(payload, dict) or payload.get("job_id") != job_id:
335
+ raise LocalizationReleaseBlocked("plan.job.invalid")
336
+ return plan_id, _validated_job_payload(payload)
337
+
338
+
339
+ def _validated_job_payload(payload: Any) -> dict[str, Any]:
340
+ try:
341
+ return _WORKER._validated_job(payload)
342
+ except _WORKER.LocalizationWorkerBlocked:
343
+ raise LocalizationReleaseBlocked("plan.job.invalid") from None
344
+
345
+
346
+ def _validate_result(job: dict[str, Any], result: Any) -> dict[str, Any]:
347
+ if not isinstance(result, dict):
348
+ raise LocalizationReleaseBlocked("result.invalid")
349
+ expected = {
350
+ "schema", "worker_schema", "job_id", "source_sha256", "target_sha256",
351
+ "source_locale", "target_locale", "content_type", "glossary_version",
352
+ "policy_version", "provider", "software_version", "candidate",
353
+ "quality_passes", "integrity", "review_confidence",
354
+ "quality_profile", "commercial_review", "human_review_required",
355
+ "independent_review_required", "release_required",
356
+ }
357
+ if set(result) != expected:
358
+ raise LocalizationReleaseBlocked("result.invalid")
359
+ candidate = result.get("candidate")
360
+ if not isinstance(candidate, str) or not candidate or len(candidate.encode("utf-8")) > MAX_TEXT_BYTES:
361
+ raise LocalizationReleaseBlocked("result.invalid")
362
+ if "\x00" in candidate or not unicodedata.is_normalized("NFC", candidate):
363
+ raise LocalizationReleaseBlocked("result.invalid")
364
+ binding = {
365
+ "job_id": job["job_id"],
366
+ "source_sha256": job["source"]["sha256"],
367
+ "source_locale": job["source"]["locale"],
368
+ "target_locale": job["target"]["locale"],
369
+ "content_type": job["content_type"],
370
+ "glossary_version": job["glossary_version"],
371
+ "policy_version": job["policy_version"],
372
+ "provider": job["provider"],
373
+ "software_version": job["software_version"],
374
+ }
375
+ if any(result.get(name) != value for name, value in binding.items()):
376
+ raise LocalizationReleaseBlocked("result.binding_mismatch")
377
+ expected_quality_profile = {
378
+ "locale": job["target"]["locale"],
379
+ "version": job["target"]["quality_profile_version"],
380
+ "sha256": job["target"]["quality_profile_sha256"],
381
+ }
382
+ if result.get("quality_profile") != expected_quality_profile:
383
+ raise LocalizationReleaseBlocked("result.quality_profile.invalid")
384
+ if result.get("schema") != _WORKER.RESULT_SCHEMA or result.get("worker_schema") != _WORKER.WORKER_SCHEMA:
385
+ raise LocalizationReleaseBlocked("result.schema.invalid")
386
+ if result.get("target_sha256") != _hash_text(candidate):
387
+ raise LocalizationReleaseBlocked("result.target_hash.invalid")
388
+ phases = result.get("quality_passes")
389
+ if not isinstance(phases, list) or [item.get("phase") for item in phases if isinstance(item, dict)] != list(_WORKER.PHASES):
390
+ raise LocalizationReleaseBlocked("result.quality.invalid")
391
+ for item in phases:
392
+ if set(item) != {"phase", "request_sha256", "response_sha256", "status"}:
393
+ raise LocalizationReleaseBlocked("result.quality.invalid")
394
+ if item["status"] != "PASS" or HEX64.fullmatch(str(item["request_sha256"])) is None:
395
+ raise LocalizationReleaseBlocked("result.quality.invalid")
396
+ if HEX64.fullmatch(str(item["response_sha256"])) is None:
397
+ raise LocalizationReleaseBlocked("result.quality.invalid")
398
+ if result.get("integrity") != {"status": "PASS", "guard": "translate-native-structure-and-token-gate"}:
399
+ raise LocalizationReleaseBlocked("result.integrity.invalid")
400
+ if result.get("release_required") is not True:
401
+ raise LocalizationReleaseBlocked("result.release.invalid")
402
+ review_confidence = result.get("review_confidence")
403
+ if (
404
+ not isinstance(review_confidence, dict)
405
+ or set(review_confidence) != {"target_native", "source_fidelity"}
406
+ or any(value not in {"high", "low"} for value in review_confidence.values())
407
+ ):
408
+ raise LocalizationReleaseBlocked("result.review_confidence.invalid")
409
+ human_review_required = result.get("human_review_required")
410
+ expected_human_review = job["content_type"] == "legal"
411
+ if not isinstance(human_review_required, bool) or human_review_required is not expected_human_review:
412
+ raise LocalizationReleaseBlocked("result.human_review.invalid")
413
+ independent_review_required = result.get("independent_review_required")
414
+ expected_independent_review = (
415
+ job["content_type"] != "legal" and "low" in review_confidence.values()
416
+ )
417
+ if (
418
+ not isinstance(independent_review_required, bool)
419
+ or independent_review_required is not expected_independent_review
420
+ ):
421
+ raise LocalizationReleaseBlocked("result.independent_review.invalid")
422
+ commercial_review = result.get("commercial_review")
423
+ if job["content_type"] == "commercial":
424
+ try:
425
+ _WORKER._COMMERCIAL.validate_summary(
426
+ commercial_review,
427
+ job["commercial_profile"],
428
+ review_required=independent_review_required,
429
+ )
430
+ except _WORKER._COMMERCIAL.CommercialReviewBlocked:
431
+ raise LocalizationReleaseBlocked("result.commercial_review.invalid") from None
432
+ elif commercial_review is not None:
433
+ raise LocalizationReleaseBlocked("result.commercial_review.invalid")
434
+ return result
435
+
436
+
437
+ class LocalizationReleaseStore:
438
+ """Append-only approvals and exact-version translation-memory lookups."""
439
+
440
+ def __init__(self, connection: sqlite3.Connection, queue: Any):
441
+ if not isinstance(connection, sqlite3.Connection):
442
+ raise LocalizationReleaseBlocked("release.connection.invalid")
443
+ if not isinstance(queue, _QUEUE.LocalizationQueue):
444
+ raise LocalizationReleaseBlocked("release.queue.invalid")
445
+ self.connection = connection
446
+ self.connection.row_factory = sqlite3.Row
447
+ self.queue = queue
448
+ version = int(connection.execute("PRAGMA user_version").fetchone()[0])
449
+ if version not in {0, SCHEMA_VERSION}:
450
+ raise LocalizationReleaseBlocked("release.schema.unsupported")
451
+ if version == 0:
452
+ with _transaction(connection):
453
+ connection.execute("""
454
+ CREATE TABLE localization_approvals (
455
+ approval_id TEXT PRIMARY KEY,
456
+ job_id TEXT NOT NULL,
457
+ target_locale TEXT NOT NULL,
458
+ target_sha256 TEXT NOT NULL,
459
+ result_json TEXT NOT NULL,
460
+ result_sha256 TEXT NOT NULL,
461
+ approval_json TEXT NOT NULL,
462
+ approval_sha256 TEXT NOT NULL,
463
+ signature_algorithm TEXT NOT NULL,
464
+ key_id TEXT NOT NULL,
465
+ signature TEXT NOT NULL,
466
+ approved_at REAL NOT NULL,
467
+ expires_at REAL NOT NULL
468
+ )
469
+ """)
470
+ connection.execute("""
471
+ CREATE INDEX localization_approvals_job
472
+ ON localization_approvals (job_id, approved_at DESC)
473
+ """)
474
+ connection.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
475
+ columns = tuple(row["name"] for row in connection.execute("PRAGMA table_info(localization_approvals)"))
476
+ if columns != _COLUMNS:
477
+ raise LocalizationReleaseBlocked("release.schema.altered")
478
+
479
+ def validated_result(self, plan: Any, job_id: str) -> dict[str, Any]:
480
+ """Return an exact completed result only after all release checks pass."""
481
+ job_id = _text(job_id, "job.id.invalid")
482
+ _, job = _plan_job(plan, job_id)
483
+ result = _validate_result(job, self.queue.result(job_id))
484
+ return json.loads(_canonical_json(result))
485
+
486
+ def approve(
487
+ self,
488
+ plan: Any,
489
+ job_id: str,
490
+ quality_receipt: str,
491
+ quality_verifier: QualityReceiptVerifier,
492
+ authority: ApprovalAuthority,
493
+ *,
494
+ now: float | int,
495
+ ttl_seconds: float | int = 2_592_000,
496
+ human_review_receipt: str | None = None,
497
+ human_review_verifier: QualityReceiptVerifier | None = None,
498
+ independent_model_review: dict[str, Any] | None = None,
499
+ independent_model_review_verifier: IndependentModelReviewVerifier | None = None,
500
+ ) -> ApprovedLocalization:
501
+ job_id = _text(job_id, "job.id.invalid")
502
+ _, job = _plan_job(plan, job_id)
503
+ result = self.validated_result(plan, job_id)
504
+ now = _timestamp(now, "approval.time.invalid")
505
+ ttl = _timestamp(ttl_seconds, "approval.ttl.invalid")
506
+ if ttl <= 0 or ttl > MAX_TTL_SECONDS:
507
+ raise LocalizationReleaseBlocked("approval.ttl.invalid")
508
+ result_json = _canonical_json(result)
509
+ result_hash = _hash_text(result_json)
510
+ quality_receipt = _receipt(quality_receipt, "quality.receipt.invalid")
511
+ verifier = getattr(quality_verifier, "verify", None)
512
+ if not callable(verifier):
513
+ raise LocalizationReleaseBlocked("quality.verifier.invalid")
514
+ try:
515
+ quality_ok = verifier(
516
+ binding=_receipt_binding(
517
+ job_id,
518
+ job,
519
+ result,
520
+ result_hash,
521
+ review_kind="quality",
522
+ ),
523
+ receipt=quality_receipt,
524
+ ) is True
525
+ except Exception as error:
526
+ _verification_failure(error, "quality")
527
+ quality_ok = False
528
+ if not quality_ok:
529
+ raise LocalizationReleaseBlocked("quality.receipt.rejected")
530
+
531
+ human_hash = None
532
+ independent_binding = None
533
+ if result["human_review_required"]:
534
+ if independent_model_review is not None:
535
+ raise LocalizationReleaseBlocked("independent_model_review.legal_forbidden")
536
+ human_review_receipt = _receipt(human_review_receipt, "human.receipt.required")
537
+ human_verify = getattr(human_review_verifier, "verify", None)
538
+ if not callable(human_verify):
539
+ raise LocalizationReleaseBlocked("human.verifier.required")
540
+ try:
541
+ human_ok = human_verify(
542
+ binding=_receipt_binding(
543
+ job_id,
544
+ job,
545
+ result,
546
+ result_hash,
547
+ review_kind="qualified_human",
548
+ ),
549
+ receipt=human_review_receipt,
550
+ ) is True
551
+ except Exception as error:
552
+ _verification_failure(error, "human")
553
+ human_ok = False
554
+ if not human_ok:
555
+ raise LocalizationReleaseBlocked("human.receipt.rejected")
556
+ human_hash = _hash_text(human_review_receipt)
557
+ elif result["independent_review_required"]:
558
+ if human_review_receipt is not None and independent_model_review is not None:
559
+ raise LocalizationReleaseBlocked("review.escalation.ambiguous")
560
+ if independent_model_review is not None:
561
+ provider, model_receipt = _independent_model_review(
562
+ independent_model_review, result["provider"],
563
+ )
564
+ model_verify = getattr(independent_model_review_verifier, "verify", None)
565
+ if not callable(model_verify):
566
+ raise LocalizationReleaseBlocked("independent_model_review.verifier.required")
567
+ try:
568
+ model_ok = model_verify(
569
+ binding=_receipt_binding(
570
+ job_id,
571
+ job,
572
+ result,
573
+ result_hash,
574
+ review_kind="independent_model",
575
+ review_provider=provider,
576
+ ),
577
+ receipt=model_receipt,
578
+ ) is True
579
+ except Exception as error:
580
+ _verification_failure(error, "independent_model_review")
581
+ model_ok = False
582
+ if not model_ok:
583
+ raise LocalizationReleaseBlocked("independent_model_review.receipt.rejected")
584
+ independent_binding = {
585
+ "schema": INDEPENDENT_MODEL_REVIEW_SCHEMA,
586
+ "provider": provider,
587
+ "receipt_sha256": _hash_text(model_receipt),
588
+ }
589
+ else:
590
+ human_review_receipt = _receipt(human_review_receipt, "human.receipt.required")
591
+ human_verify = getattr(human_review_verifier, "verify", None)
592
+ if not callable(human_verify):
593
+ raise LocalizationReleaseBlocked("human.verifier.required")
594
+ try:
595
+ human_ok = human_verify(
596
+ binding=_receipt_binding(
597
+ job_id,
598
+ job,
599
+ result,
600
+ result_hash,
601
+ review_kind="qualified_human",
602
+ ),
603
+ receipt=human_review_receipt,
604
+ ) is True
605
+ except Exception as error:
606
+ _verification_failure(error, "human")
607
+ human_ok = False
608
+ if not human_ok:
609
+ raise LocalizationReleaseBlocked("human.receipt.rejected")
610
+ human_hash = _hash_text(human_review_receipt)
611
+ elif any(value is not None for value in (
612
+ human_review_receipt, human_review_verifier,
613
+ independent_model_review, independent_model_review_verifier,
614
+ )):
615
+ raise LocalizationReleaseBlocked("review.escalation.unexpected")
616
+
617
+ immutable = {
618
+ "schema": APPROVAL_SCHEMA,
619
+ "job_id": job_id,
620
+ "source_sha256": result["source_sha256"],
621
+ "target_sha256": result["target_sha256"],
622
+ "source_locale": result["source_locale"],
623
+ "target_locale": result["target_locale"],
624
+ "content_type": result["content_type"],
625
+ "glossary_version": result["glossary_version"],
626
+ "policy_version": result["policy_version"],
627
+ "provider": result["provider"],
628
+ "software_version": result["software_version"],
629
+ "worker_schema": result["worker_schema"],
630
+ "review_confidence": result["review_confidence"],
631
+ "quality_profile": result["quality_profile"],
632
+ "result_sha256": result_hash,
633
+ "quality_receipt_sha256": _hash_text(quality_receipt),
634
+ "human_review_receipt_sha256": human_hash,
635
+ "independent_model_review": independent_binding,
636
+ }
637
+ approval_id = "blun-l10n-approval-" + _hash_text(_canonical_json(immutable))
638
+ payload = {
639
+ **immutable,
640
+ "approval_id": approval_id,
641
+ "approved_at": now,
642
+ "expires_at": now + ttl,
643
+ }
644
+ approval_json = _canonical_json(payload)
645
+ approval_bytes = approval_json.encode("utf-8")
646
+ prior = self.connection.execute(
647
+ "SELECT target_sha256 FROM localization_approvals WHERE job_id = ? LIMIT 1",
648
+ (job_id,),
649
+ ).fetchone()
650
+ if prior is not None and prior["target_sha256"] != result["target_sha256"]:
651
+ raise LocalizationReleaseBlocked("translation_memory.immutable")
652
+ sign = getattr(authority, "sign", None)
653
+ verify = getattr(authority, "verify", None)
654
+ if not callable(sign) or not callable(verify):
655
+ raise LocalizationReleaseBlocked("approval.authority.invalid")
656
+ try:
657
+ signed = _signature(sign(approval_bytes))
658
+ verified = verify(approval_bytes, signed) is True
659
+ except LocalizationReleaseBlocked:
660
+ raise
661
+ except Exception:
662
+ raise LocalizationReleaseBlocked("approval.signing.failed") from None
663
+ if not verified:
664
+ raise LocalizationReleaseBlocked("approval.signature.rejected")
665
+
666
+ with _transaction(self.connection):
667
+ prior = self.connection.execute(
668
+ "SELECT target_sha256 FROM localization_approvals WHERE job_id = ? LIMIT 1",
669
+ (job_id,),
670
+ ).fetchone()
671
+ if prior is not None and prior["target_sha256"] != result["target_sha256"]:
672
+ raise LocalizationReleaseBlocked("translation_memory.immutable")
673
+ existing = self.connection.execute(
674
+ "SELECT * FROM localization_approvals WHERE approval_id = ?",
675
+ (approval_id,),
676
+ ).fetchone()
677
+ if existing is None:
678
+ self.connection.execute("""
679
+ INSERT INTO localization_approvals VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
680
+ """, (
681
+ approval_id, job_id, result["target_locale"], result["target_sha256"],
682
+ result_json, result_hash, approval_json, _hash_text(approval_json),
683
+ signed.algorithm, signed.key_id, signed.signature, now, now + ttl,
684
+ ))
685
+ else:
686
+ return self._approved_from_row(existing, plan, authority, now)
687
+ return self.lookup(plan, job_id, authority, now=now)
688
+
689
+ def _approved_from_row(
690
+ self,
691
+ row: sqlite3.Row,
692
+ plan: Any,
693
+ authority: ApprovalAuthority,
694
+ now: float,
695
+ ) -> ApprovedLocalization:
696
+ _, job = _plan_job(plan, row["job_id"])
697
+ approved, _ = self._verified_row(row, job, authority, now)
698
+ return approved
699
+
700
+ def _verified_row(
701
+ self,
702
+ row: sqlite3.Row,
703
+ job: dict[str, Any],
704
+ authority: ApprovalAuthority,
705
+ now: float,
706
+ ) -> tuple[ApprovedLocalization, dict[str, Any]]:
707
+ if _hash_text(row["result_json"]) != row["result_sha256"]:
708
+ raise LocalizationReleaseBlocked("translation_memory.result_tampered")
709
+ if _hash_text(row["approval_json"]) != row["approval_sha256"]:
710
+ raise LocalizationReleaseBlocked("approval.payload.tampered")
711
+ try:
712
+ result = json.loads(row["result_json"])
713
+ payload = json.loads(row["approval_json"])
714
+ except json.JSONDecodeError:
715
+ raise LocalizationReleaseBlocked("translation_memory.json.invalid") from None
716
+ result = _validate_result(job, result)
717
+ expected_keys = {
718
+ "schema", "job_id", "source_sha256", "target_sha256", "source_locale",
719
+ "target_locale", "content_type", "glossary_version", "policy_version",
720
+ "provider", "software_version", "worker_schema", "result_sha256",
721
+ "review_confidence",
722
+ "quality_profile",
723
+ "quality_receipt_sha256", "human_review_receipt_sha256",
724
+ "independent_model_review", "approval_id",
725
+ "approved_at", "expires_at",
726
+ }
727
+ if set(payload) != expected_keys:
728
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
729
+ binding = {
730
+ "schema": APPROVAL_SCHEMA,
731
+ "job_id": row["job_id"],
732
+ "source_sha256": result["source_sha256"],
733
+ "target_sha256": result["target_sha256"],
734
+ "source_locale": result["source_locale"],
735
+ "target_locale": result["target_locale"],
736
+ "content_type": result["content_type"],
737
+ "glossary_version": result["glossary_version"],
738
+ "policy_version": result["policy_version"],
739
+ "provider": result["provider"],
740
+ "software_version": result["software_version"],
741
+ "worker_schema": result["worker_schema"],
742
+ "review_confidence": result["review_confidence"],
743
+ "quality_profile": result["quality_profile"],
744
+ "result_sha256": row["result_sha256"],
745
+ "approved_at": row["approved_at"],
746
+ "expires_at": row["expires_at"],
747
+ }
748
+ if any(payload.get(name) != value for name, value in binding.items()):
749
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
750
+ if payload.get("approval_id") != row["approval_id"]:
751
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
752
+ if HEX64.fullmatch(str(payload.get("quality_receipt_sha256"))) is None:
753
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
754
+ human_hash = payload.get("human_review_receipt_sha256")
755
+ independent = payload.get("independent_model_review")
756
+ if not (
757
+ result["human_review_required"] or result["independent_review_required"]
758
+ ) and (human_hash is not None or independent is not None):
759
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
760
+ if result["human_review_required"]:
761
+ if HEX64.fullmatch(str(human_hash)) is None or independent is not None:
762
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
763
+ elif result["independent_review_required"]:
764
+ if (human_hash is None) == (independent is None):
765
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
766
+ elif human_hash is not None and HEX64.fullmatch(str(human_hash)) is None:
767
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
768
+ elif independent is not None:
769
+ if not isinstance(independent, dict) or set(independent) != {
770
+ "schema", "provider", "receipt_sha256",
771
+ }:
772
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
773
+ if independent.get("schema") != INDEPENDENT_MODEL_REVIEW_SCHEMA:
774
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
775
+ provider = independent.get("provider")
776
+ if not isinstance(provider, dict) or set(provider) != {"id", "model_id", "model_version"}:
777
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
778
+ if any(not isinstance(value, str) or not value for value in provider.values()):
779
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
780
+ if provider["id"] == result["provider"]["id"]:
781
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
782
+ if HEX64.fullmatch(str(independent.get("receipt_sha256"))) is None:
783
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
784
+ immutable = {
785
+ key: value
786
+ for key, value in payload.items()
787
+ if key not in {"approval_id", "approved_at", "expires_at"}
788
+ }
789
+ expected_id = "blun-l10n-approval-" + _hash_text(_canonical_json(immutable))
790
+ if expected_id != row["approval_id"]:
791
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
792
+ if row["target_locale"] != result["target_locale"] or row["target_sha256"] != result["target_sha256"]:
793
+ raise LocalizationReleaseBlocked("approval.binding_mismatch")
794
+ signature = _signature(ApprovalSignature(
795
+ row["signature_algorithm"], row["key_id"], row["signature"],
796
+ ))
797
+ verify = getattr(authority, "verify", None)
798
+ try:
799
+ valid = callable(verify) and verify(row["approval_json"].encode("utf-8"), signature) is True
800
+ except Exception:
801
+ valid = False
802
+ if not valid:
803
+ raise LocalizationReleaseBlocked("approval.signature.invalid")
804
+ if row["expires_at"] <= now:
805
+ raise LocalizationReleaseBlocked("approval.expired")
806
+ return ApprovedLocalization(
807
+ approval_id=row["approval_id"],
808
+ job_id=row["job_id"],
809
+ target_locale=row["target_locale"],
810
+ target_sha256=row["target_sha256"],
811
+ candidate=result["candidate"],
812
+ release_evidence=validate_publication_evidence({
813
+ "schema": PUBLICATION_EVIDENCE_SCHEMA,
814
+ "job_id": row["job_id"],
815
+ "target_locale": row["target_locale"],
816
+ "target_sha256": row["target_sha256"],
817
+ "approval_id": row["approval_id"],
818
+ "content_type": result["content_type"],
819
+ "result_sha256": row["result_sha256"],
820
+ "approval_sha256": row["approval_sha256"],
821
+ "quality_receipt_sha256": payload["quality_receipt_sha256"],
822
+ "commercial_profile": job.get("commercial_profile"),
823
+ "commercial_review": json.loads(
824
+ _canonical_json(result["commercial_review"])
825
+ ),
826
+ }),
827
+ approved_at=float(row["approved_at"]),
828
+ expires_at=float(row["expires_at"]),
829
+ ), result
830
+
831
+ def verified_result_cache(
832
+ self,
833
+ authority: ApprovalAuthority,
834
+ ) -> VerifiedResultCache:
835
+ """Bind the exact approval verifier for use by a queue runner."""
836
+ return VerifiedResultCache(self, authority)
837
+
838
+ def cached_result(
839
+ self,
840
+ job_payload: dict[str, Any],
841
+ authority: ApprovalAuthority,
842
+ *,
843
+ now: float | int,
844
+ ) -> dict[str, Any] | None:
845
+ """Return an exact verified result, or ``None`` for a clean cache miss."""
846
+ job = _validated_job_payload(job_payload)
847
+ now = _timestamp(now, "approval.time.invalid")
848
+ row = self.connection.execute("""
849
+ SELECT * FROM localization_approvals
850
+ WHERE job_id = ? ORDER BY approved_at DESC, approval_id DESC LIMIT 1
851
+ """, (job["job_id"],)).fetchone()
852
+ if row is None:
853
+ return None
854
+ try:
855
+ _, result = self._verified_row(row, job, authority, now)
856
+ except LocalizationReleaseBlocked as error:
857
+ if error.code == "approval.expired":
858
+ return None
859
+ raise
860
+ return json.loads(_canonical_json(result))
861
+
862
+ def lookup(
863
+ self,
864
+ plan: Any,
865
+ job_id: str,
866
+ authority: ApprovalAuthority,
867
+ *,
868
+ now: float | int,
869
+ ) -> ApprovedLocalization:
870
+ job_id = _text(job_id, "job.id.invalid")
871
+ _plan_job(plan, job_id)
872
+ now = _timestamp(now, "approval.time.invalid")
873
+ row = self.connection.execute("""
874
+ SELECT * FROM localization_approvals
875
+ WHERE job_id = ? ORDER BY approved_at DESC, approval_id DESC LIMIT 1
876
+ """, (job_id,)).fetchone()
877
+ if row is None:
878
+ raise LocalizationReleaseBlocked("approval.missing")
879
+ return self._approved_from_row(row, plan, authority, now)
880
+
881
+ def readiness(
882
+ self,
883
+ plan: Any,
884
+ authority: ApprovalAuthority,
885
+ *,
886
+ now: float | int,
887
+ ) -> WebsiteReadiness:
888
+ plan_id = _text(getattr(plan, "plan_id", None), "plan.invalid")
889
+ jobs = getattr(plan, "jobs", None)
890
+ if not isinstance(jobs, tuple) or not jobs:
891
+ raise LocalizationReleaseBlocked("plan.invalid")
892
+ now = _timestamp(now, "approval.time.invalid")
893
+ required = tuple(sorted(job.as_payload()["target"]["locale"] for job in jobs))
894
+ approved: list[str] = []
895
+ blocked: list[tuple[str, str]] = []
896
+ for job in jobs:
897
+ locale = job.as_payload()["target"]["locale"]
898
+ try:
899
+ self.lookup(plan, job.job_id, authority, now=now)
900
+ except LocalizationReleaseBlocked as error:
901
+ blocked.append((locale, error.code))
902
+ else:
903
+ approved.append(locale)
904
+ return WebsiteReadiness(
905
+ plan_id=plan_id,
906
+ ready=not blocked and tuple(sorted(approved)) == required,
907
+ required_locales=required,
908
+ approved_locales=tuple(sorted(approved)),
909
+ blocked=tuple(sorted(blocked)),
910
+ )
911
+
912
+ def publication_bundle(
913
+ self,
914
+ plan: Any,
915
+ authority: ApprovalAuthority,
916
+ *,
917
+ now: float | int,
918
+ ) -> tuple[ApprovedLocalization, ...]:
919
+ status = self.readiness(plan, authority, now=now)
920
+ if not status.ready:
921
+ raise LocalizationReleaseBlocked("website.not_ready")
922
+ return tuple(sorted(
923
+ (
924
+ self.lookup(plan, job.job_id, authority, now=now)
925
+ for job in plan.jobs
926
+ ),
927
+ key=lambda item: item.target_locale,
928
+ ))